/**
 * 文件处理任务
 */
import { splitProjectFile } from '@/lib/file/text-splitter';
import { handleDomainTree } from '@/lib/util/domain-tree';
import { processPdf, getFilePageCount } from '@/lib/file/file-process/pdf';
import { getProject, updateProject } from '@/lib/db/projects';
import { TASK } from '@/constant';
import { updateTask } from './index';

/**
 * 处理文件处理任务
 * @param {Object} task 任务对象
 */
export

export default {
  processFileProcessingTask
};

async function processFileProcessingTask(task) {
  const taskMessage = {
    current: {
      fileName: '',
      processedPage: 0,
      totalPage: 0
    },
    stepInfo: '',
    processedFiles: 0,
    totalFiles: 0,
    errorList: [],
    finishedList: []
  };
  try {
    console.log(`start processing file processing task: ${task.id}`);
    const params = JSON.parse(task.note);
    const { projectId, fileList, strategy = 'default', vsionModel, domainTreeAction } = params;
    taskMessage.totalFiles = fileList.length;
    const totalPages = await getFilePageCount(projectId, fileList);
    taskMessage.stepInfo = `Total ${taskMessage.totalFiles} files to process, total ${totalPages} pages`;
    await updateTask(task.id, {
      status: TASK.STATUS.PROCESSING,
      totalCount: totalPages + 1,
      detail: JSON.stringify(taskMessage),
      startTime: new Date()
    });
    let fileResult = {
      totalChunks: 0,
      chunks: [],
      toc: ''
    };
    const project = await getProject(projectId);
    for (const file of fileList) {
      try {
        taskMessage.current.fileName = file.fileName;
        taskMessage.current.processedPage = 1;
        taskMessage.current.totalPage = file.pageCount || 1;
        await updateTask(task.id, {
          status: TASK.STATUS.PROCESSING,
          totalCount: totalPages + 1,
          detail: JSON.stringify(taskMessage),
          startTime: new Date()
        });
        if (file.fileName.endsWith('.pdf')) {
          task.vsionModel = vsionModel;
          const result = await processPdf(strategy, projectId, file.fileName, {
            ...params.options,
            updateTask: updateTask,
            task: task,
            message: taskMessage
          });
          if (!result.success) {
            throw new Error(result.error || `File processing failed`);
          }
        }
        const { toc, chunks, totalChunks } = await splitProjectFile(projectId, file);
        fileResult.toc += toc;
        fileResult.chunks.push(...chunks);
        fileResult.totalChunks += totalChunks;
        console.log(projectId, file.fileName, `${file.fileName} Text split completed`);
        taskMessage.finishedList.push(file);
        taskMessage.processedFiles++;
        await updateTask(task.id, {
          completedCount: task.completedCount + file.pageCount,
          detail: JSON.stringify(taskMessage),
          updateAt: new Date()
        });
        task.completedCount += file.pageCount;
      } catch (error) {
        const errorMessage = `Processing file ${file.fileName} failed: ${error.message}`;
        taskMessage.errorList.push(errorMessage);
        console.error(errorMessage);
        await updateTask(task.id, {
          detail: JSON.stringify(taskMessage)
        });
      }
    }
    console.log('domainTreeAction', domainTreeAction);
    try {
      const tags = await handleDomainTree({
        projectId,
        newToc: fileResult.toc,
        model: JSON.parse(task.modelInfo),
        language: task.language,
        action: domainTreeAction,
        fileList,
        project
      });
      if (!tags && domainTreeAction !== 'keep') {
        await updateProject(projectId, { ...project });
        return NextResponse.json(
          { error: 'AI analysis failed, please check model configuration, delete file and retry!' },
          { status: 400 }
        );
      }
      console.log(`File processing completed successfully`);
      taskMessage.stepInfo = `File processing completed successfully`;
      await updateTask(task.id, {
        completedCount: task.totalCount,
        status: TASK.STATUS.COMPLETED,
        detail: JSON.stringify(taskMessage)
      });
    } catch (error) {
      console.error(`processing failed:`, error);
      taskMessage.stepInfo = `File processing failed: ${error.message}`;
      await updateTask(task.id, {
        status: TASK.STATUS.FAILED,
        completedCount: 0,
        detail: JSON.stringify(taskMessage),
        endTime: new Date()
      });
      return;
    }
    console.log(`task ${task.id} finished`);
  } catch (error) {
    console.error('pdf processing failed:', error);
    taskMessage.stepInfo = `File processing failed: ${String(error)}`;
    await updateTask(task.id, {
      status: TASK.STATUS.FAILED,
      detail: JSON.stringify(taskMessage),
      endTime: new Date()
    });
  }
}

import http from 'http';
import https from 'https';
import AdmZip from 'adm-zip';
import { getProjectRoot } from '@/lib/db/base';
import fs from 'fs';
import path from 'path';

// 常量定义
const MINERU_API_BASE = 'https://mineru.net/api/v4';
const POLL_INTERVAL = 3000; // 3秒
const MAX_POLL_ATTEMPTS = 90; // 最多尝试90次
const PROCESSING_STATES = {
  DONE: 'done',
  FAILED: 'failed'
};

export async function minerUProcessing(projectId, fileName, options = {}) {
  console.log('executing pdf mineru conversion strategy......');
  try {
    const { updateTask, task, message } = options;

    let taskCompletedCount = task.completedCount;

    // 获取项目路径
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);
    const filePath = path.join(projectPath, 'files', fileName);

    // 读取任务配置
    const taskConfigPath = path.join(projectPath, 'task-config.json');
    let taskConfig;
    try {
      await fs.promises.access(taskConfigPath);
      const taskConfigData = await fs.promises.readFile(taskConfigPath, 'utf8');
      taskConfig = JSON.parse(taskConfigData);
    } catch (error) {
      console.error('error getting mineru token configuration:', error);
      throw new Error('token configuration not found, please check if mineru token is configured in task settings');
    }

    const key = taskConfig?.minerUToken;
    if (key === undefined || key === null || key === '') {
      throw new Error('token configuration not found, please check if mineru token is configured in task settings');
    }

    // 准备请求选项
    const requestOptions = JSON.stringify({
      enable_formula: true,
      layout_model: 'doclayout_yolo',
      enable_table: true,
      files: [{ name: fileName, is_ocr: true, data_id: 'abcd' }]
    });

    // 1. 获取文件上传地址
    console.log('mineru getting file upload url...');
    const urlResponse = await makeHttpRequest(`${MINERU_API_BASE}/file-urls/batch`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(requestOptions),
        Authorization: `Bearer ${key}`
      },
      body: requestOptions
    });

    if (urlResponse.code !== 0 || !urlResponse.data?.file_urls?.[0]) {
      throw new Error('failed to get file upload url: ' + JSON.stringify(urlResponse));
    }

    //上传文件后会自动执行任务
    let batchId = null;
    let uploadUrl = null;
    console.log('mineru executing file upload task...');
    if (urlResponse.code == 0) {
      //上传文件地址
      uploadUrl = urlResponse.data?.file_urls?.[0];
      //此次任务id
      batchId = urlResponse.data?.batch_id;
    }

    // 2. 上传文件
    await uploadFile(filePath, uploadUrl);
    console.log('mineru file upload completed!');

    // 3. 轮询查询转换状态
    console.log('mineru starting to check task progress...');
    let currentPage = 0;
    let totalPage = 0;
    while (true) {
      try {
        //查询任务进度API
        const resultResponse = await makeHttpRequest(`${MINERU_API_BASE}/extract-results/batch/${batchId}`, {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${key}`
          }
        });

        // 任务状态
        const currentState = resultResponse.data?.extract_result?.[0]?.state;
        const extract_progress = resultResponse.data?.extract_result?.[0]?.extract_progress;

        if (extract_progress) {
          // 任务进度
          currentPage = extract_progress.extracted_pages;
          // 总页数
          totalPage = extract_progress.total_pages;
        } else {
          currentPage = totalPage;
        }
        message.current.processedPage = currentPage;
        message.stepInfo = `processing ${fileName} ${currentPage}/${totalPage} pages progress: ${(currentPage / totalPage) * 100}%`;

        //更新任务状态
        await updateTask(task.id, {
          completedCount: currentPage + taskCompletedCount,
          detail: JSON.stringify(message)
        });

        console.log(`mineru ${fileName} current progress: ${currentPage}/${totalPage}, status: ${currentState}`);

        //解析成功结束回写状态定时器
        if (resultResponse.code === 0 && currentState === PROCESSING_STATES.DONE) {
          const zipUrl = resultResponse.data.extract_result[0].full_zip_url;
          const savePath = path.join(projectPath, 'files');
          await downloadAndExtractZip(zipUrl, savePath, fileName);
          break;
        }
        // 检查是否失败
        if (resultResponse.code !== 0 || currentState === PROCESSING_STATES.FAILED) {
          throw new Error(`task processing failed: ${JSON.stringify(resultResponse)}`);
        }

        // 等待下次轮询
        await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
      } catch (error) {
        throw error;
      }
    }
    console.log('mineru pdf conversion completed!');
    return { success: true };
  } catch (error) {
    console.error('mineru api call error:', error);
    throw error;
  }
}

/**
 * 发送 HTTP 请求
 */
async function makeHttpRequest(url, options) {
  return new Promise((resolve, reject) => {
    const isHttps = url.startsWith('https');
    const client = isHttps ? https : http;

    const urlObj = new URL(url);
    const requestOptions = {
      hostname: urlObj.hostname,
      port: urlObj.port || (isHttps ? 443 : 80),
      path: `${urlObj.pathname}${urlObj.search}`,
      method: options.method,
      headers: options.headers
    };

    const req = client.request(requestOptions, res => {
      let data = '';

      res.on('data', chunk => {
        data += chunk;
      });

      res.on('end', () => {
        try {
          if (res.statusCode >= 200 && res.statusCode < 300) {
            resolve(JSON.parse(data));
          } else {
            reject(new Error(`request failed, status code: ${res.statusCode}, response: ${data}`));
          }
        } catch (error) {
          reject(new Error('failed to parse response'));
        }
      });
    });

    req.on('error', error => {
      reject(error);
    });

    if (options.body) {
      req.write(options.body);
    }

    req.end();
  });
}

/**
 * 上传文件至MinerU指定地址
 */
async function uploadFile(filePath, uploadUrl) {
  return new Promise((resolve, reject) => {
    const isHttps = uploadUrl.startsWith('https');
    const url = new URL(uploadUrl);
    const client = url.protocol === 'https:' ? https : http;
    const fileStream = fs.createReadStream(filePath);
    const options = {
      hostname: url.hostname,
      port: url.port || (isHttps ? 443 : 80),
      path: `${url.pathname}${url.search}`,
      method: 'PUT'
    };

    const req = client.request(options, res => {
      let responseData = '';

      res.on('data', chunk => {
        responseData += chunk;
      });

      res.on('end', () => {
        if (res.statusCode === 200) {
          resolve(responseData);
        } else {
          reject(new Error(`Upload failed with status ${res.statusCode}: ${responseData}`));
        }
      });
    });

    req.on('error', error => {
      reject(error);
    });

    fileStream.pipe(req);
  });
}

/**
 * 获取任务执行完成后的压缩包，仅解压md文件
 */
async function downloadAndExtractZip(zipUrl, targetDir, fileName) {
  // 创建目标目录
  if (!fs.existsSync(targetDir)) {
    fs.mkdirSync(targetDir, { recursive: true });
  }

  // 下载 ZIP 文件到内存
  const zipBuffer = await new Promise((resolve, reject) => {
    https.get(zipUrl, res => {
      const chunks = [];
      res.on('data', chunk => chunks.push(chunk));
      res.on('end', () => resolve(Buffer.concat(chunks)));
      res.on('error', reject);
    });
  });

  // 解压到目标目录
  const zip = new AdmZip(zipBuffer);
  const zipEntries = zip.getEntries();
  zipEntries.forEach(entry => {
    if (entry.entryName.toLowerCase().endsWith('.md')) {
      // 获取文件内容为 Buffer
      const content = zip.readFile(entry);
      // 尝试用 UTF-8 解码，如果失败则尝试其他编码
      const text = content.toString('utf8');
      // 创建输出文件路径
      const outputPath = path.join(targetDir, fileName.replace('.pdf', '.md'));
      // 写入文件，确保使用 UTF-8 编码
      fs.writeFileSync(outputPath, text, { encoding: 'utf8' });
      console.log(`extracted to directory: ${outputPath}`);
    }
  });
}

export default {
  minerUProcessing
};

/**
 * 问题生成任务处理服务
 */

import { PrismaClient } from '@prisma/client';
import { processInParallel } from '@/lib/util/async';
import { updateTask } from './index';
import { getTaskConfig } from '@/lib/db/projects';
import questionService from '@/lib/services/questions';

const prisma = new PrismaClient();

/**
 * 处理问题生成任务
 * @param {object} task - 任务对象
 * @returns {Promise<void>}
 */
export async function processQuestionGenerationTask(task) {
  try {
    console.log(`开始处理问题生成任务: ${task.id}`);

    // 解析模型信息
    let modelInfo;
    try {
      modelInfo = JSON.parse(task.modelInfo);
    } catch (error) {
      throw new Error(`模型信息解析失败: ${error.message}`);
    }

    // 获取项目配置
    const taskConfig = await getTaskConfig(task.projectId);
    const concurrencyLimit = taskConfig?.concurrencyLimit || 2;

    // 1. 查询所有未生成问题的文本块（过滤掉名出为 Distilled Content 的文本块）
    const chunks = await prisma.chunks.findMany({
      where: {
        projectId: task.projectId,
        // 过滤掉名称为 Distilled Content 的文本块
        NOT: {
          name: {
            contains: 'Distilled Content'
          }
        }
      },
      include: {
        Questions: true
      }
    });

    // 过滤出没有问题的文本块
    const chunksWithoutQuestions = chunks.filter(chunk => chunk.Questions.length === 0);

    if (chunksWithoutQuestions.length === 0) {
      console.log(`项目 ${task.projectId} 没有需要生成问题的文本块`);
      await updateTask(task.id, {
        status: 1,
        completedCount: 0,
        totalCount: 0,
        note: '没有需要生成问题的文本块'
      });
      return;
    }

    // 更新任务总数
    const totalCount = chunksWithoutQuestions.length;
    await updateTask(task.id, {
      totalCount,
      detail: `待处理文本块数量: ${totalCount}`
    });

    // 2. 批量处理每个文本块
    let successCount = 0;
    let errorCount = 0;
    let totalQuestions = 0;
    let latestTaskStatus = 0;

    // 单个文本块处理函数
    const processChunk = async chunk => {
      try {
        // 如果任务已经被标记为失败或已中断，不再继续处理
        const latestTask = await prisma.task.findUnique({ where: { id: task.id } });
        if (latestTask.status === 2 || latestTask.status === 3) {
          latestTaskStatus = latestTask.status;
          return;
        }

        const data = await questionService.generateQuestionsForChunkWithGA(task.projectId, chunk.id, {
          model: modelInfo,
          language: task.language === 'zh-CN' ? '中文' : 'en',
          enableGaExpansion: true // 启用GA扩展
        });
        console.log(`文本块 ${chunk.id} 已生成 ${data.total || 0} 个问题`);

        // 增加成功计数
        successCount++;
        totalQuestions += data.total || 0;

        // 更新任务进度
        await updateTask(task.id, {
          completedCount: successCount + errorCount,
          detail: `已处理: ${successCount + errorCount}/${totalCount}, 成功: ${successCount}, 失败: ${errorCount}, 共生成问题: ${totalQuestions}`
        });

        return { success: true, chunkId: chunk.id, total: data.total || 0 };
      } catch (error) {
        console.error(`处理文本块 ${chunk.id} 出错:`, error);
        errorCount++;

        // 更新任务进度
        await updateTask(task.id, {
          completedCount: successCount + errorCount,
          detail: `已处理: ${successCount + errorCount}/${totalCount}, 成功: ${successCount}, 失败: ${errorCount}, 共生成问题: ${totalQuestions}`
        });

        return { success: false, chunkId: chunk.id, error: error.message };
      }
    };

    // 并行处理所有文本块，使用任务设置中的并发限制
    await processInParallel(chunksWithoutQuestions, processChunk, concurrencyLimit, async (completed, total) => {
      console.log(`问题生成进度: ${completed}/${total}`);
    });

    if (!latestTaskStatus) {
      // 任务完成，更新状态
      const finalStatus = errorCount > 0 && successCount === 0 ? 2 : 1; // 如果全部失败，标记为失败；否则标记为完成
      await updateTask(task.id, {
        status: finalStatus,
        detail: '',
        note: '',
        endTime: new Date()
      });
    }

    console.log(`问题生成任务 ${task.id} 处理完成`);
  } catch (error) {
    console.error(`问题生成任务处理失败: ${task.id}`, error);
    await updateTask(task.id, {
      status: 2,
      detail: `处理失败: ${error.message}`,
      note: `处理失败: ${error.message}`
    });
  }
}

/**
 * 答案生成任务处理器
 * 负责异步处理答案生成任务，获取所有未生成答案的问题并批量处理
 */

import { PrismaClient } from '@prisma/client';
import { processInParallel } from '@/lib/util/async';
import { updateTask } from './index';
import datasetService from '@/lib/services/datasets';
import { getTaskConfig } from '@/lib/db/projects';

const prisma = new PrismaClient();

/**
 * 处理答案生成任务
 * 查询未生成答案的问题并批量处理
 * @param {Object} task 任务对象
 * @returns {Promise<void>}
 */
export async function processAnswerGenerationTask(task) {
  try {
    console.log(`开始处理答案生成任务: ${task.id}`);

    // 解析模型信息
    let modelInfo;
    try {
      modelInfo = JSON.parse(task.modelInfo);
    } catch (error) {
      throw new Error(`模型信息解析失败: ${error.message}`);
    }

    // 从任务对象直接获取项目 ID
    const projectId = task.projectId;

    // 1. 查询未生成答案的问题
    console.log(`开始处理项目 ${projectId} 的答案生成任务`);
    const questionsWithoutAnswers = await prisma.questions.findMany({
      where: {
        projectId,
        answered: false // 未生成答案的问题
      }
    });

    // 如果没有需要处理的问题，直接完成任务
    if (questionsWithoutAnswers.length === 0) {
      await updateTask(task.id, {
        status: 1, // 1 表示完成
        detail: '没有需要处理的问题',
        note: '',
        endTime: new Date()
      });
      return;
    }

    // 获取任务配置，包括并发限制
    const taskConfig = await getTaskConfig(projectId);
    const concurrencyLimit = taskConfig.concurrencyLimit || 3;

    // 更新任务总数
    const totalCount = questionsWithoutAnswers.length;
    await updateTask(task.id, {
      totalCount,
      detail: `待处理问题数量: ${totalCount}`,
      note: ''
    });

    // 2. 批量处理每个问题
    let successCount = 0;
    let errorCount = 0;
    let totalDatasets = 0;
    let latestTaskStatus = 0;

    // 单个问题处理函数
    const processQuestion = async question => {
      try {
        // 如果任务已经被标记为失败或已中断，不再继续处理
        const latestTask = await prisma.task.findUnique({ where: { id: task.id } });
        if (latestTask.status === 2 || latestTask.status === 3) {
          latestTaskStatus = latestTask.status;
          return;
        }

        // 调用数据集生成服务生成答案
        const result = await datasetService.generateDatasetForQuestion(task.projectId, question.id, {
          model: modelInfo,
          language: task.language === 'zh-CN' ? '中文' : 'en'
        });
        console.log(`问题 ${question.id} 已生成答案，数据集 ID: ${result.dataset.id}`);

        // 增加成功计数
        successCount++;
        totalDatasets++;

        // 更新任务进度
        const progressNote = `已处理: ${successCount + errorCount}/${totalCount}, 成功: ${successCount}, 失败: ${errorCount}, 共生成数据集: ${totalDatasets}`;
        await updateTask(task.id, {
          completedCount: successCount + errorCount,
          detail: progressNote,
          note: progressNote
        });

        return { success: true, questionId: question.id, datasetId: result.dataset.id };
      } catch (error) {
        console.error(`处理问题 ${question.id} 出错:`, error);
        errorCount++;

        // 更新任务进度
        const progressNote = `已处理: ${successCount + errorCount}/${totalCount}, 成功: ${successCount}, 失败: ${errorCount}, 共生成数据集: ${totalDatasets}`;
        await updateTask(task.id, {
          completedCount: successCount + errorCount,
          detail: progressNote,
          note: progressNote
        });

        return { success: false, questionId: question.id, error: error.message };
      }
    };

    // 并行处理所有问题，使用任务设置中的并发限制
    await processInParallel(questionsWithoutAnswers, processQuestion, concurrencyLimit, async (completed, total) => {
      console.log(`答案生成进度: ${completed}/${total}`);
    });

    if (!latestTaskStatus) {
      // 任务完成，更新状态
      const finalStatus = errorCount > 0 && successCount === 0 ? 2 : 1; // 如果全部失败，标记为失败；否则标记为完成
      await updateTask(task.id, {
        status: finalStatus,
        completedCount: successCount + errorCount,
        detail: '',
        note: '',
        endTime: new Date()
      });
    }

    console.log(`任务 ${task.id} 已完成`);
  } catch (error) {
    console.error('处理答案生成任务出错:', error);
    await updateTask(task.id, {
      status: 2, // 2 表示失败
      detail: `处理失败: ${error.message}`,
      note: `处理失败: ${error.message}`,
      endTime: new Date()
    });
  }
}

export default {
  processAnswerGenerationTask
};

import { getProjectRoot } from '@/lib/db/base';
import { getTaskConfig } from '@/lib/db/projects';
import convertPrompt from '@/lib/llm/prompts/pdfToMarkdown';
import convertPromptEn from '@/lib/llm/prompts/pdfToMarkdownEn';
import reTitlePrompt from '@/lib/llm/prompts/optimalTitle';
import reTitlePromptEn from '@/lib/llm/prompts/optimalTitleEn';
import path from 'path';

export async function visionProcessing(projectId, fileName, options = {}) {
  try {
    const { updateTask, task, message } = options;

    let taskCompletedCount = task.completedCount;

    console.log('executing vision conversion strategy......');

    // 获取项目路径
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);
    const filePath = path.join(projectPath, 'files', fileName);

    // 获取项目配置
    const taskConfig = await getTaskConfig(projectId);

    const model = task.vsionModel;

    if (!model) {
      throw new Error('please check if pdf conversion vision model is configured');
    }

    if (model.type !== 'vision') {
      throw new Error(
        `${model.modelName}(${model.providerName}) this model is not a vision model, please check [model configuration]`
      );
    }

    if (!model.apiKey) {
      throw new Error(
        `${model.modelName}(${model.providerName}) this model has no api key configured, please check [model configuration]`
      );
    }

    const convert = task.language === 'en' ? convertPromptEn : convertPrompt;
    const reTitle = task.language === 'en' ? reTitlePromptEn : reTitlePrompt;

    //创建临时文件夹分割不同任务产生的临时图片文件，防止同时读写一个文件夹，导致内容出错
    const config = {
      pdfPath: filePath,
      outputDir: path.join(projectPath, 'files'),
      apiKey: model.apiKey,
      model: model.modelId,
      baseUrl: model.endpoint,
      useFullPage: true,
      verbose: false,
      concurrency: taskConfig.visionConcurrencyLimit,
      prompt: convert(),
      textPrompt: reTitle(),
      onProgress: async ({ current, total, taskStatus }) => {
        if (updateTask && task.id) {
          message.current.processedPage = current;
          message.setpInfo = `processing ${fileName} ${current}/${total} pages progress: ${(current / total) * 100}% `;
          await updateTask(task.id, {
            completedCount: taskCompletedCount + current,
            detail: JSON.stringify(message)
          });
        }
      }
    };

    console.log('vision strategy: starting pdf file processing');

    const { parsePdf } = await import('pdf2md-js');
    await parsePdf(filePath, config);

    //转换结束
    return { success: true };
  } catch (error) {
    console.error('vision strategy processing error:', error);
    throw error;
  }
}

export default {
  visionProcessing
};

import { NextResponse } from 'next/server';
import { splitProjectFile, getProjectChunks } from '@/lib/file/text-splitter';
import { getProject, updateProject } from '@/lib/db/projects';
import { getTags } from '@/lib/db/tags';
import { handleDomainTree } from '@/lib/util/domain-tree';

// 处理文本分割请求
export async function POST(request, { params }) {
  try {
    const { projectId } = params;

    // 获取请求体
    const { fileNames, model, language, domainTreeAction = 'rebuild' } = await request.json();

    if (!model) {
      return NextResponse.json({ error: 'Pelease Select Model' }, { status: 400 });
    }

    const project = await getProject(projectId);

    let result = {
      totalChunks: 0,
      chunks: [],
      toc: ''
    };
    for (let i = 0; i < fileNames.length; i++) {
      const fileName = fileNames[i];
      // 分割文本
      const { toc, chunks, totalChunks } = await splitProjectFile(projectId, fileName);
      result.toc += toc;
      result.chunks.push(...chunks);
      result.totalChunks += totalChunks;
      console.log(projectId, fileName, `Text split completed, ${domainTreeAction} domain tree`);
    }

    // 调用领域树处理模块
    const tags = await handleDomainTree({
      projectId,
      action: domainTreeAction,
      newToc: result.toc,
      model,
      language,
      fileNames,
      project
    });

    if (!tags && domainTreeAction !== 'keep') {
      await updateProject(projectId, { ...project });
      return NextResponse.json(
        { error: 'AI analysis failed, please check model configuration, delete file and retry!' },
        { status: 400 }
      );
    }

    return NextResponse.json({ ...result, tags });
  } catch (error) {
    console.error('Text split error:', String(error));
    return NextResponse.json({ error: error.message || 'Text split failed' }, { status: 500 });
  }
}

// 获取项目中的所有文本块
export async function GET(request, { params }) {
  try {
    const { projectId } = params;
    const { searchParams } = new URL(request.url);
    const filter = searchParams.get('filter');
    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
    }

    // 获取文本块详细信息
    const result = await getProjectChunks(projectId, filter);

    const tags = await getTags(projectId);

    // 返回详细的文本块信息和文件结果（单个文件）
    return NextResponse.json({
      chunks: result.chunks,
      ...result.fileResult, // 单个文件结果，而不是数组
      tags
    });
  } catch (error) {
    console.error('Failed to get text chunks:', String(error));
    return NextResponse.json({ error: error.message || 'Failed to get text chunks' }, { status: 500 });
  }
}

/**
 * 任务服务层入口文件
 * 根据任务类型分配处理函数
 */

import { PrismaClient } from '@prisma/client';
import { TASK } from '@/constant';
import { processQuestionGenerationTask } from './question-generation';
import { processAnswerGenerationTask } from './answer-generation';
import { processFileProcessingTask } from './file-processing';
import './recovery';

const prisma = new PrismaClient();

/**
 * 处理异步任务
 * @param {string} taskId - 任务ID
 * @returns {Promise<void>}
 */
export async function processTask(taskId) {
  try {
    // 获取任务信息
    const task = await prisma.task.findUnique({
      where: { id: taskId }
    });

    if (!task) {
      console.error(`任务不存在: ${taskId}`);
      return;
    }

    // 如果任务已经完成或失败，不再处理
    if (task.status === TASK.STATUS.COMPLETED || task.status === TASK.STATUS.FAILED) {
      console.log(`任务已处理完成，无需再次执行: ${taskId}`);
      return;
    }

    // 根据任务类型调用相应的处理函数
    switch (task.taskType) {
      case 'question-generation':
        await processQuestionGenerationTask(task);
        break;
      case 'file-processing':
        await processFileProcessingTask(task);
        break;
      case 'answer-generation':
        await processAnswerGenerationTask(task);
        break;
      case 'data-distillation':
        // 暂未实现
        console.log('数据蒸馏任务暂未实现');
        await updateTask(taskId, { status: 2, note: '数据蒸馏任务暂未实现' });
        break;
      default:
        console.error(`未知任务类型: ${task.taskType}`);
        await updateTask(taskId, { status: TASK.STATUS.FAILED, note: `未知任务类型: ${task.taskType}` });
    }
  } catch (error) {
    console.error(`处理任务失败: ${taskId}`, String(error));
    await updateTask(taskId, { status: TASK.STATUS.FAILED, note: `处理失败: ${error.message}` });
  }
}

/**
 * 更新任务状态
 * @param {string} taskId - 任务ID
 * @param {object} data - 更新数据
 * @returns {Promise<object>} - 更新后的任务
 */
export async function updateTask(taskId, data) {
  try {
    // 如果更新状态为完成或失败，且未提供结束时间，则自动添加
    if ((data.status === TASK.STATUS.COMPLETED || data.status === TASK.STATUS.FAILED) && !data.endTime) {
      data.endTime = new Date();
    }

    // 更新任务
    const updatedTask = await prisma.task.update({
      where: { id: taskId },
      data
    });

    return updatedTask;
  } catch (error) {
    console.error(`更新任务状态失败: ${taskId}`, error);
    throw error;
  }
}

/**
 * 启动任务处理器
 * 轮询数据库中的待处理任务并执行
 */
export async function startTaskProcessor() {
  try {
    console.log('启动任务处理器...');

    // 查找所有处理中的任务
    const pendingTasks = await prisma.task.findMany({
      where: { status: TASK.STATUS.PROCESSING }
    });

    if (pendingTasks.length > 0) {
      console.log(`发现 ${pendingTasks.length} 个待处理任务`);

      // 处理所有待处理任务
      for (const task of pendingTasks) {
        console.log(`开始处理任务: ${task.id}`);
        processTask(task.id).catch(err => {
          console.error(`任务处理失败: ${task.id}`, err);
        });
      }
    } else {
      console.log('没有待处理的任务');
    }
  } catch (error) {
    console.error('启动任务处理器失败', error);
  }
}

'use server';

import fs from 'fs';
import path from 'path';
import { getProjectRoot, ensureDir } from '../db/base';
import { getProject } from '@/lib/db/projects';
import { getChunkByProjectId, saveChunks } from '@/lib/db/chunks';
const {
  TokenTextSplitter,
  CharacterTextSplitter,
  RecursiveCharacterTextSplitter
} = require('@langchain/textsplitters');
const { Document } = require('@langchain/core/documents');

// 导入Markdown分割工具
const markdownSplitter = require('./split-markdown/index');

async function splitFileByType({ projectPath, fileContent, fileName, projectId, fileId }) {
  // 获取任务配置
  const taskConfigPath = path.join(projectPath, 'task-config.json');
  let taskConfig;

  try {
    await fs.promises.access(taskConfigPath);
    const taskConfigData = await fs.promises.readFile(taskConfigPath, 'utf8');
    taskConfig = JSON.parse(taskConfigData);
  } catch (error) {
    taskConfig = {
      textSplitMinLength: 1500,
      textSplitMaxLength: 2000
    };
  }
  // 获取分割参数
  const minLength = taskConfig.textSplitMinLength || 1500;
  const maxLength = taskConfig.textSplitMaxLength || 2000;
  const chunkSize = taskConfig.chunkSize || 1500;
  const chunkOverlap = taskConfig.chunkOverlap || 200;
  const separator = taskConfig.separator || '\n\n';
  const separators = taskConfig.separators || ['|', '##', '>', '-'];
  const splitLanguage = taskConfig.splitType || 'js';
  const splitType = taskConfig.splitType;

  if (splitType === 'text') {
    // 字符分块
    const textSplitter = new CharacterTextSplitter({
      separator,
      chunkSize,
      chunkOverlap
    });
    const splitResult = await textSplitter.createDocuments([fileContent]);
    return splitResult.map((part, index) => {
      const chunkId = `${path.basename(fileName, path.extname(fileName))}-part-${index + 1}`;
      return {
        projectId,
        name: chunkId,
        fileId,
        fileName,
        content: part.pageContent,
        summary: '',
        size: part.pageContent.length
      };
    });
  } else if (splitType === 'token') {
    // Token 分块
    const textSplitter = new TokenTextSplitter({
      chunkSize,
      chunkOverlap
    });
    const splitResult = await textSplitter.splitText(fileContent);
    return splitResult.map((part, index) => {
      const chunkId = `${path.basename(fileName, path.extname(fileName))}-part-${index + 1}`;
      return {
        projectId,
        name: chunkId,
        fileId,
        fileName,
        content: part,
        summary: '',
        size: part.length
      };
    });
  } else if (splitType === 'code') {
    // 递归分块
    const textSplitter = new RecursiveCharacterTextSplitter({
      chunkSize,
      chunkOverlap,
      separators
    });
    const jsSplitter = RecursiveCharacterTextSplitter.fromLanguage(splitLanguage, {
      chunkSize,
      chunkOverlap
    });
    const splitResult = await jsSplitter.createDocuments([fileContent]);
    return splitResult.map((part, index) => {
      const chunkId = `${path.basename(fileName, path.extname(fileName))}-part-${index + 1}`;
      return {
        projectId,
        name: chunkId,
        fileId,
        fileName,
        content: part.pageContent,
        summary: '',
        size: part.pageContent.length
      };
    });
  } else if (splitType === 'recursive') {
    // 递归分块
    const textSplitter = new RecursiveCharacterTextSplitter({
      chunkSize,
      chunkOverlap,
      separators
    });
    const splitResult = await textSplitter.splitDocuments([new Document({ pageContent: fileContent })]);
    return splitResult.map((part, index) => {
      const chunkId = `${path.basename(fileName, path.extname(fileName))}-part-${index + 1}`;
      return {
        projectId,
        name: chunkId,
        fileId,
        fileName,
        content: part.pageContent,
        summary: '',
        size: part.pageContent.length
      };
    });
  } else {
    // 默认采用之前的分块方法
    const splitResult = markdownSplitter.splitMarkdown(fileContent, minLength, maxLength);
    return splitResult.map((part, index) => {
      const chunkId = `${path.basename(fileName, path.extname(fileName))}-part-${index + 1}`;
      return {
        projectId,
        name: chunkId,
        fileId,
        fileName,
        content: part.content,
        summary: part.summary,
        size: part.content.length
      };
    });
  }
}

/**
 * 分割项目中的Markdown文件
 * @param {string} projectId - 项目ID
 * @param {string} fileName - 文件名
 * @returns {Promise<Array>} - 分割结果数组
 */
export async function splitProjectFile(projectId, file) {
  const { fileName, fileId } = file;
  try {
    // 获取项目根目录
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);
    let filePath = path.join(projectPath, 'files', fileName);

    if (!filePath.endsWith('.md')) {
      filePath = path.join(projectPath, 'files', fileName.replace(/\.[^/.]+$/, '.md'));
    }
    try {
      await fs.promises.access(filePath);
    } catch (error) {
      throw new Error(`文件 ${fileName} 不存在`);
    }

    // 读取文件内容
    const fileContent = await fs.promises.readFile(filePath, 'utf8');

    // 保存分割结果到chunks目录
    const savedChunks = await splitFileByType({ projectPath, fileContent, fileName, projectId, fileId });
    await saveChunks(savedChunks);

    // 提取目录结构（如果需要所有文件的内容拼接后再提取目录）
    const tocJSON = markdownSplitter.extractTableOfContents(fileContent);
    const toc = markdownSplitter.tocToMarkdown(tocJSON, { isNested: true });

    // 保存目录结构到单独的toc文件夹
    const tocDir = path.join(projectPath, 'toc');
    await ensureDir(tocDir);
    const tocPath = path.join(tocDir, `${path.basename(fileName, path.extname(fileName))}-toc.json`);
    await fs.promises.writeFile(tocPath, JSON.stringify(tocJSON, null, 2));

    return {
      fileName,
      totalChunks: savedChunks.length,
      chunks: savedChunks,
      toc
    };
  } catch (error) {
    console.error('文本分割出错:', error);
    throw error;
  }
}

/**
 * 获取项目中的所有文本块
 * @param {string} projectId - 项目ID
 * @returns {Promise<Array>} - 文本块详细信息数组
 */
export async function getProjectChunks(projectId, filter) {
  try {
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);
    const tocDir = path.join(projectPath, 'toc');
    const project = await getProject(projectId);

    let chunks = await getChunkByProjectId(projectId, filter);
    // 读取所有TOC文件
    const tocByFile = {};
    let toc = '';
    try {
      await fs.promises.access(tocDir);
      const tocFiles = await fs.promises.readdir(tocDir);

      for (const tocFile of tocFiles) {
        if (tocFile.endsWith('-toc.json')) {
          const tocPath = path.join(tocDir, tocFile);
          const tocContent = await fs.promises.readFile(tocPath, 'utf8');
          const fileName = tocFile.replace('-toc.json', '.md');

          try {
            tocByFile[fileName] = JSON.parse(tocContent);
            toc += '# File：' + fileName + '\n';
            toc += markdownSplitter.tocToMarkdown(tocByFile[fileName], { isNested: true }) + '\n';
          } catch (e) {
            console.error(`解析TOC文件 ${tocFile} 出错:`, e);
          }
        }
      }
    } catch (error) {
      // TOC目录不存在或读取出错，继续处理
    }
    // 整合结果
    let fileResult = {
      fileName: project.name + '.md',
      totalChunks: chunks.length,
      chunks,
      toc
    };

    return {
      fileResult, // 单个文件结果，而不是数组
      chunks
    };
  } catch (error) {
    console.error('获取文本块出错:', error);
    throw error;
  }
}

/**
 * 获取项目中的所有目录
 * @param {string} projectId - 项目ID
 */
export async function getProjectTocs(projectId) {
  try {
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);
    const tocDir = path.join(projectPath, 'toc');

    // 读取所有TOC文件
    const tocByFile = {};
    let toc = '';
    try {
      await fs.promises.access(tocDir);
      const tocFiles = await fs.promises.readdir(tocDir);

      for (const tocFile of tocFiles) {
        if (tocFile.endsWith('-toc.json')) {
          const tocPath = path.join(tocDir, tocFile);
          const tocContent = await fs.promises.readFile(tocPath, 'utf8');
          const fileName = tocFile.replace('-toc.json', '.md');

          try {
            tocByFile[fileName] = JSON.parse(tocContent);
            toc += '# File：' + fileName + '\n';
            toc += markdownSplitter.tocToMarkdown(tocByFile[fileName], { isNested: true }) + '\n';
          } catch (e) {
            console.error(`解析TOC文件 ${tocFile} 出错:`, e);
          }
        }
      }
    } catch (error) {
      // TOC目录不存在或读取出错，继续处理
    }

    return toc;
  } catch (error) {
    console.error('获取文本块出错:', error);
    throw error;
  }
}

/**
 * 指定文件的目录
 */
export async function getProjectTocByName(projectId, fileName) {
  try {
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);
    const tocDir = path.join(projectPath, 'toc');

    // 读取所有TOC文件
    const tocByFile = {};
    let toc = '';
    try {
      await fs.promises.access(tocDir);
      const tocFiles = await fs.promises.readdir(tocDir);

      for (const tocFile of tocFiles) {
        if (tocFile.endsWith(fileName.replace('.md', '') + '-toc.json')) {
          const tocPath = path.join(tocDir, tocFile);
          const tocContent = await fs.promises.readFile(tocPath, 'utf8');
          const fileName = tocFile.replace('-toc.json', '.md');

          try {
            tocByFile[fileName] = JSON.parse(tocContent);
            toc += '# File：' + fileName + '\n';
            toc += markdownSplitter.tocToMarkdown(tocByFile[fileName], { isNested: true }) + '\n';
          } catch (e) {
            console.error(`解析TOC文件 ${tocFile} 出错:`, e);
          }
        }
      }
    } catch (error) {
      // TOC目录不存在或读取出错，继续处理
    }

    return toc;
  } catch (error) {
    console.error('获取文本块出错:', error);
    throw error;
  }
}

import { NextResponse } from 'next/server';
import path from 'path';
import fs from 'fs/promises';
import { getProjectRoot } from '@/lib/db/base';
import { getTaskConfig } from '@/lib/db/projects';
import { processTask } from '@/lib/services/tasks';
import { db } from '@/lib/db/index';

// 获取任务配置
export async function GET(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目 ID
    if (!projectId) {
      return NextResponse.json({ error: 'Project ID is required' }, { status: 400 });
    }

    // 获取项目根目录
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);

    // 检查项目是否存在
    try {
      await fs.access(projectPath);
    } catch (error) {
      return NextResponse.json({ error: 'Project does not exist' + projectPath }, { status: 404 });
    }

    const taskConfig = await getTaskConfig(projectId);
    return NextResponse.json(taskConfig);
  } catch (error) {
    console.error('Failed to obtain task configuration:', String(error));
    return NextResponse.json({ error: 'Failed to obtain task configuration' }, { status: 500 });
  }
}

// 更新任务配置
export async function PUT(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目 ID
    if (!projectId) {
      return NextResponse.json({ error: 'Project ID is required' }, { status: 400 });
    }

    // 获取请求体
    const taskConfig = await request.json();

    // 验证请求体
    if (!taskConfig) {
      return NextResponse.json({ error: 'Task configuration cannot be empty' }, { status: 400 });
    }

    // 获取项目根目录
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);

    // 检查项目是否存在
    try {
      await fs.access(projectPath);
    } catch (error) {
      return NextResponse.json({ error: 'Project does not exist' }, { status: 404 });
    }

    // 获取任务配置文件路径
    const taskConfigPath = path.join(projectPath, 'task-config.json');

    // 写入任务配置文件
    await fs.writeFile(taskConfigPath, JSON.stringify(taskConfig, null, 2), 'utf-8');

    return NextResponse.json({ message: 'Task configuration updated successfully' });
  } catch (error) {
    console.error('Failed to update task configuration:', String(error));
    return NextResponse.json({ error: 'Failed to update task configuration' }, { status: 500 });
  }
}

// 创建新任务
export async function POST(request, { params }) {
  try {
    const { projectId } = params;
    const data = await request.json();

    // 验证必填字段
    const { taskType, modelInfo, language, detail = '', totalCount = 0, note } = data;

    if (!taskType) {
      return NextResponse.json(
        {
          code: 400,
          error: 'Missing required parameter: taskType'
        },
        { status: 400 }
      );
    }

    // 创建新任务
    const newTask = await db.task.create({
      data: {
        projectId,
        taskType,
        status: 0, // 初始状态: 处理中
        modelInfo: typeof modelInfo === 'string' ? modelInfo : JSON.stringify(modelInfo),
        language: language || 'zh-CN',
        detail: detail || '',
        totalCount,
        note: note ? JSON.stringify(note) : '',
        completedCount: 0
      }
    });

    // 异步启动任务处理
    processTask(newTask.id).catch(err => {
      console.error(`Task startup failed: ${newTask.id}`, String(err));
    });

    return NextResponse.json({
      code: 0,
      data: newTask,
      message: 'Task created successfully'
    });
  } catch (error) {
    console.error('Failed to create task:', String(error));
    return NextResponse.json(
      {
        code: 500,
        error: 'Failed to create task',
        message: error.message
      },
      { status: 500 }
    );
  }
}

'use client';

import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Paper, Grid } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { useAtomValue } from 'jotai/index';
import { selectedModelInfoAtom } from '@/lib/store';
import UploadArea from './components/UploadArea';
import FileList from './components/FileList';
import DeleteConfirmDialog from './components/DeleteConfirmDialog';
import PdfProcessingDialog from './components/PdfProcessingDialog';
import DomainTreeActionDialog from './components/DomainTreeActionDialog';
import FileLoadingProgress from './components/FileLoadingProgress';
import { fileApi, taskApi } from '@/lib/api';
import { getContent, checkMaxSize, checkInvalidFiles, getvalidFiles } from '@/lib/file/file-process';
import { toast } from 'sonner';

export default function FileUploader({
  projectId,
  onUploadSuccess,
  onFileDeleted,
  sendToPages,
  setPdfStrategy,
  pdfStrategy,
  selectedViosnModel,
  setSelectedViosnModel,
  setPageLoading,
  taskFileProcessing,
  fileTask
}) {
  const theme = useTheme();
  const { t } = useTranslation();
  const [files, setFiles] = useState([]);
  const [pdfFiles, setPdfFiles] = useState([]);
  const [uploadedFiles, setUploadedFiles] = useState({});
  const selectedModelInfo = useAtomValue(selectedModelInfoAtom);
  const [uploading, setUploading] = useState(false);
  const [loading, setLoading] = useState(false);
  const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
  const [pdfProcessConfirmOpen, setpdfProcessConfirmOpen] = useState(false);
  const [fileToDelete, setFileToDelete] = useState({});
  const [domainTreeActionOpen, setDomainTreeActionOpen] = useState(false);
  const [domainTreeAction, setDomainTreeAction] = useState('');
  const [isFirstUpload, setIsFirstUpload] = useState(false);
  const [pendingAction, setPendingAction] = useState(null);
  const [taskSettings, setTaskSettings] = useState(null);
  const [visionModels, setVisionModels] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const [pageSize] = useState(10);
  const [searchFileName, setSearchFileName] = useState('');

  useEffect(() => {
    fetchUploadedFiles();
  }, [currentPage, searchFileName]);

  /**
   * 处理 PDF 处理方式选择
   */
  const handleRadioChange = event => {
    const modelId = event.target.selectedVision;
    setPdfStrategy(event.target.value);

    if (event.target.value === 'mineru') {
      toast.success(t('textSplit.mineruSelected'));
    } else if (event.target.value === 'vision') {
      const model = visionModels.find(item => item.id === modelId);
      toast.success(
        t('textSplit.customVisionModelSelected', {
          name: model.modelName,
          provider: model.projectName
        })
      );
    } else {
      toast.success(t('textSplit.defaultSelected'));
    }
  };

  /**
   * 获取上传的文件列表
   * @param {*} page
   * @param {*} size
   * @param {*} fileName
   */
  const fetchUploadedFiles = async (page = currentPage, size = pageSize, fileName = searchFileName) => {
    try {
      setLoading(true);
      const data = await fileApi.getFiles({ projectId, page, size, fileName, t });
      setUploadedFiles(data);

      setIsFirstUpload(data.total === 0);

      const taskData = await taskApi.getProjectTasks(projectId);
      setTaskSettings(taskData);

      //使用Jotai会出现数据获取的延迟，导致这里模型获取不到，改用localStorage获取模型信息
      const model = JSON.parse(localStorage.getItem('modelConfigList'));

      //过滤出视觉模型
      const visionItems = model.filter(item => item.type === 'vision' && item.apiKey);

      //先默认选择第一个配置的视觉模型
      if (visionItems.length > 0) {
        setSelectedViosnModel(visionItems[0].id);
      }

      setVisionModels(visionItems);
    } catch (error) {
      toast.error(error.message);
    } finally {
      setLoading(false);
    }
  };

  /**
   * 处理文件选择
   */
  const handleFileSelect = event => {
    const selectedFiles = Array.from(event.target.files);

    checkMaxSize(selectedFiles);
    checkInvalidFiles(selectedFiles);

    const validFiles = getvalidFiles(selectedFiles);

    if (validFiles.length > 0) {
      setFiles(prev => [...prev, ...validFiles]);
    }
    const hasPdfFiles = selectedFiles.filter(file => file.name.endsWith('.pdf'));
    if (hasPdfFiles.length > 0) {
      setpdfProcessConfirmOpen(true);
      setPdfFiles(hasPdfFiles);
    }
  };

  /**
   * 从待上传文件列表中移除文件
   */
  const removeFile = index => {
    const fileToRemove = files[index];
    setFiles(prev => prev.filter((_, i) => i !== index));
    if (fileToRemove && fileToRemove.name.toLowerCase().endsWith('.pdf')) {
      setPdfFiles(prevPdfFiles => prevPdfFiles.filter(pdfFile => pdfFile.name !== fileToRemove.name));
    }
  };

  /**
   * 上传文件
   */
  const uploadFiles = async () => {
    if (files.length === 0) return;

    // 如果是第一次上传，直接走默认逻辑
    if (isFirstUpload) {
      handleStartUpload('rebuild');
      return;
    }

    // 否则打开领域树操作选择对话框
    setDomainTreeAction('upload');
    setPendingAction({ type: 'upload' });
    setDomainTreeActionOpen(true);
  };

  /**
   * 处理领域树操作选择
   */
  const handleDomainTreeAction = action => {
    setDomainTreeActionOpen(false);

    // 执行挂起的操作
    if (pendingAction && pendingAction.type === 'upload') {
      handleStartUpload(action);
    } else if (pendingAction && pendingAction.type === 'delete') {
      handleDeleteFile(action);
    }

    // 清除挂起的操作
    setPendingAction(null);
  };

  /**
   * 开始上传文件
   */
  const handleStartUpload = async domainTreeActionType => {
    setUploading(true);
    try {
      const uploadedFileInfos = [];
      for (const file of files) {
        const { fileContent, fileName } = await getContent(file);
        const data = await fileApi.uploadFile({ file, projectId, fileContent, fileName, t });
        uploadedFileInfos.push({ fileName: data.fileName, fileId: data.fileId });
      }
      toast.success(t('textSplit.uploadSuccess', { count: files.length }));
      setFiles([]);
      setCurrentPage(1);
      await fetchUploadedFiles();
      if (onUploadSuccess) {
        await onUploadSuccess(uploadedFileInfos, pdfFiles, domainTreeActionType);
      }
    } catch (err) {
      toast.error(err.message || t('textSplit.uploadFailed'));
    } finally {
      setUploading(false);
    }
  };

  // 打开删除确认对话框
  const openDeleteConfirm = (fileId, fileName) => {
    setFileToDelete({ fileId, fileName });
    setDeleteConfirmOpen(true);
  };

  // 关闭删除确认对话框
  const closeDeleteConfirm = () => {
    setDeleteConfirmOpen(false);
    setFileToDelete(null);
  };

  // 删除文件前确认领域树操作
  const confirmDeleteFile = () => {
    setDeleteConfirmOpen(false);

    // 如果没有其他文件了（删除后会变为空），直接删除
    if (uploadedFiles.total <= 1) {
      handleDeleteFile('keep');
      return;
    }

    // 否则打开领域树操作选择对话框
    setDomainTreeAction('delete');
    setPendingAction({ type: 'delete' });
    setDomainTreeActionOpen(true);
  };

  // 处理删除文件
  const handleDeleteFile = async domainTreeActionType => {
    if (!fileToDelete) return;

    try {
      setLoading(true);
      closeDeleteConfirm();

      await fileApi.deleteFile({
        fileToDelete,
        projectId,
        domainTreeActionType,
        modelInfo: selectedModelInfo || {},
        t
      });
      await fetchUploadedFiles();

      if (onFileDeleted) {
        const filesLength = uploadedFiles.total;
        onFileDeleted(fileToDelete, filesLength);
      }

      if (uploadedFiles.data && uploadedFiles.data.length <= 1 && currentPage > 1) {
        setCurrentPage(1);
      }

      toast.success(t('textSplit.deleteSuccess', { fileName: fileToDelete.fileName }));
    } catch (error) {
      console.error('删除文件出错:', error);
      toast.error(error.message);
    } finally {
      setLoading(false);
      setFileToDelete(null);
    }
  };

  return (
    <Paper
      elevation={0}
      sx={{
        p: 3,
        mb: 3,
        border: `1px solid ${theme.palette.divider}`,
        borderRadius: 2
      }}
    >
      {taskFileProcessing ? (
        <FileLoadingProgress fileTask={fileTask} />
      ) : (
        <>
          <Grid container spacing={3}>
            {/* 左侧：上传文件区域 */}
            <Grid item xs={10} md={5} sx={{ maxWidth: '100%', width: '100%' }}>
              <UploadArea
                theme={theme}
                files={files}
                uploading={uploading}
                uploadedFiles={uploadedFiles}
                onFileSelect={handleFileSelect}
                onRemoveFile={removeFile}
                onUpload={uploadFiles}
                selectedModel={selectedModelInfo}
              />
            </Grid>

            {/* 右侧：已上传文件列表 */}
            <Grid item xs={14} md={7} sx={{ maxWidth: '100%', width: '100%' }}>
              <FileList
                theme={theme}
                files={uploadedFiles}
                loading={loading}
                setPageLoading={setPageLoading}
                sendToFileUploader={array => sendToPages(array)}
                onDeleteFile={openDeleteConfirm}
                projectId={projectId}
                currentPage={currentPage}
                onPageChange={(page, fileName) => {
                  if (fileName !== undefined) {
                    // 搜索时更新搜索关键词和页码
                    setSearchFileName(fileName);
                    setCurrentPage(page);
                  } else {
                    // 翻页时只更新页码
                    setCurrentPage(page);
                  }
                }}
              />
            </Grid>
          </Grid>

          <DeleteConfirmDialog
            open={deleteConfirmOpen}
            fileName={fileToDelete?.fileName}
            onClose={closeDeleteConfirm}
            onConfirm={confirmDeleteFile}
          />

          {/* 领域树操作选择对话框 */}
          <DomainTreeActionDialog
            open={domainTreeActionOpen}
            onClose={() => setDomainTreeActionOpen(false)}
            onConfirm={handleDomainTreeAction}
            isFirstUpload={isFirstUpload}
            action={domainTreeAction}
          />
          {/* 检测到pdf的处理框 */}
          <PdfProcessingDialog
            open={pdfProcessConfirmOpen}
            onClose={() => setpdfProcessConfirmOpen(false)}
            onRadioChange={handleRadioChange}
            value={pdfStrategy}
            projectId={projectId}
            taskSettings={taskSettings}
            visionModels={visionModels}
            selectedViosnModel={selectedViosnModel}
            setSelectedViosnModel={setSelectedViosnModel}
          />
        </>
      )}
    </Paper>
  );
}

import { getProjectRoot, readJsonFile } from '@/lib/db/base';
import fs from 'fs';
import path from 'path';
import { db } from '@/lib/db/index';
import { DEFAULT_MODEL_SETTINGS } from '@/constant/model';
import { getFileMD5 } from '@/lib/util/file';

/**
 * 执行迁移操作
 * @param {Object} task 任务对象，用于更新进度
 * @returns {Promise<number>} 迁移项目数量
 */
export async function main(task = null) {
  const projectRoot = await getProjectRoot();
  let count = 0;
  const files = await fs.promises.readdir(projectRoot, { withFileTypes: true });

  // 筛选出未迁移的项目
  const unmigratedProjects = [];
  for (const file of files) {
    if (!file.isDirectory()) continue;

    const project = await db.projects.findUnique({ where: { id: file.name } });
    if (!project) {
      unmigratedProjects.push({
        file,
        projectPath: path.join(projectRoot, file.name)
      });
    }
  }

  // 如果有任务对象，初始化任务信息
  if (task) {
    task.total = unmigratedProjects.length;
    task.completed = 0;
    task.errors = [];

    if (task.total === 0) {
      task.progress = 100;
      task.status = 'completed';
      return 0;
    }
  }

  // 使用互斥锁来安全地更新计数器
  let completedCount = 0;

  // 创建并行任务，保留原有的 Promise.all 高效并行处理
  const promises = unmigratedProjects.map(async ({ file, projectPath }) => {
    try {
      const projectId = file.name;
      const project = await db.projects.findUnique({ where: { id: projectId } });
      if (!project) {
        // 再次检查项目是否已迁移
        let projectDB = await projectHandle(projectId, projectRoot, projectPath);
        if (projectDB) {
          await chunkHandle(projectId, projectPath);
          await tagsHandle(projectId, projectPath);
          await modelConfigHandle(projectId, projectPath);
          await questionHandle(projectId, projectPath);
          await datasetHandle(projectId, projectPath);
          await updateQuestions(projectId);

          // 原子操作增加完成数量
          completedCount++;

          // 更新进度
          if (task) {
            task.completed = completedCount;
            task.progress = Math.floor((completedCount / task.total) * 100);
          }

          return 1; // 返回1表示迁移了一个项目
        }
      }
      return 0;
    } catch (error) {
      console.error(`处理项目出错：${projectPath}`, error);
      if (task) {
        task.errors.push({
          projectId: file.name,
          error: error.message
        });
      }
      return 0; // 出错时返回0
    }
  });

  // 等待所有并行任务完成
  const results = await Promise.all(promises);

  // 统计总完成数量
  count = results.reduce((total, curr) => total + curr, 0);

  // 确保最终进度为100%
  if (task && count > 0) {
    task.progress = 100;
  }

  return count;
}

//备份文件
async function backupHandle(projectRoot, projectPath) {
  const projectName = path.basename(projectPath);
  const newProjectName = projectName + '-backup';
  const newProjectPath = path.join(path.dirname(projectPath), newProjectName);
  try {
    await fs.promises.rename(projectPath, newProjectPath);
    console.log(`File renamed from ${projectPath} to ${newProjectPath}`);
  } catch (error) {
    console.error(`Failed to rename file from ${projectPath} to ${newProjectPath}`, error);
  }
}

//项目文件数据处理
async function projectHandle(projectId, projectRoot, projectPath) {
  try {
    const configPath = path.join(projectPath, 'config.json');
    let projectData = await readJsonFile(configPath);
    if (!projectData) return null;
    let project = await db.projects.create({
      data: {
        id: projectId,
        name: projectData.name,
        description: projectData.description
      }
    });
    if (projectData.uploadedFiles && projectData.uploadedFiles.length > 0) {
      let projectId = project.id;
      const newProjectPath = path.join(projectRoot, projectId, 'files');
      let uploadFileList = [];
      const filesPath = path.join(projectPath, 'files');
      for (const fileName of projectData.uploadedFiles) {
        const fPath = path.join(filesPath, fileName);
        //获取文件大小
        const stats = await fs.promises.stat(fPath);
        //获取文件md5
        const md5 = await getFileMD5(fPath);
        //获取文件扩展名
        const ext = path.extname(fPath);
        let data = {
          projectId,
          fileName,
          size: stats.size,
          md5,
          fileExt: ext,
          path: newProjectPath
        };
        uploadFileList.push(data);
      }
      await db.uploadFiles.createMany({ data: uploadFileList });
    }
    return project;
  } catch (error) {
    console.error('Error project insert db:', error);
    throw error;
  }
}

//同步其他配置文件
async function syncOtherConfigFile(projectRoot, projectPath, projectNewId) {
  if (fs.existsSync(projectPath)) {
    const newProjectPath = path.join(projectRoot, projectNewId);
    try {
      await copyDirRecursive(projectPath, newProjectPath, projectNewId);
      console.log(`sync config at: ${newProjectPath}`);
    } catch (error) {
      console.error(`Failed to sync config at: ${newProjectPath}`, error);
    }
  } else {
    console.error(`Project not found at path: ${projectPath}`);
  }
}

async function tagsHandle(projectId, projectPath) {
  const tagsPath = path.join(projectPath, 'tags.json');
  try {
    if (!fs.existsSync(tagsPath)) {
      return;
    }
    const tagsData = await readJsonFile(tagsPath);
    if (tagsData.length === 0) return;
    await insertTags(projectId, tagsData);
  } catch (error) {
    console.error('Error tags.json insert db:', error);
    throw error;
  }
}

async function insertTags(projectId, tags, parentId = null) {
  for (const tag of tags) {
    // 插入当前节点
    const createdTag = await db.tags.create({
      data: {
        projectId,
        label: tag.label,
        parentId: parentId
      }
    });
    // 如果有子节点，递归插入
    if (tag.child && tag.child.length > 0) {
      await insertTags(projectId, tag.child, createdTag.id);
    }
  }
}

async function modelConfigHandle(projectId, projectPath) {
  const modelConfigPath = path.join(projectPath, 'model-config.json');
  try {
    if (!fs.existsSync(modelConfigPath)) {
      return;
    }
    const modelConfigData = await readJsonFile(modelConfigPath);
    if (modelConfigData.length === 0) return;
    let modelConfigList = [];
    for (const modelConfig of modelConfigData) {
      if (!modelConfig.name) continue;
      modelConfigList.push({
        projectId,
        providerId: modelConfig.providerId,
        providerName: modelConfig.provider,
        endpoint: modelConfig.endpoint,
        apiKey: modelConfig.apiKey,
        modelId: modelConfig.name,
        modelName: modelConfig.name,
        type: modelConfig.type ? modelConfig.type : 'text',
        maxTokens: modelConfig.maxTokens ? modelConfig.maxTokens : DEFAULT_MODEL_SETTINGS.maxTokens,
        temperature: modelConfig.temperature ? modelConfig.temperature : DEFAULT_MODEL_SETTINGS.temperature,
        topK: 0,
        topP: 0,
        status: 1
      });
    }
    return await db.modelConfig.createMany({ data: modelConfigList });
  } catch (error) {
    console.error('Error model-config.json insert db:', error);
    throw error;
  }
}

//chunk文件数据处理
async function chunkHandle(projectId, projectPath) {
  try {
    const filesPath = path.join(projectPath, 'files');
    const fileList = await safeReadDir(filesPath);

    let chunkList = [];
    for (const fileName of fileList) {
      const baseName = path.basename(fileName, path.extname(fileName));
      let file = await db.uploadFiles.findFirst({ where: { fileName, projectId } });
      const chunksPath = path.join(projectPath, 'chunks');
      const chunks = await safeReadDir(chunksPath, { withFileTypes: true });
      for (const chunk of chunks) {
        if (chunk.name.startsWith(baseName + '-part-')) {
          const content = await fs.promises.readFile(path.join(chunksPath, chunk.name), 'utf8');
          chunkList.push({
            name: path.basename(chunk.name, path.extname(chunk.name)),
            projectId,
            fileId: file !== null ? file.id : '',
            fileName,
            content,
            // TODO summary 暂时使用 content
            summary: content,
            size: content.length
          });
        }
      }
    }
    return await db.chunks.createMany({ data: chunkList });
  } catch (error) {
    console.error('Error chunk insert db:', error);
    throw error;
  }
}

//问题文件处理
async function questionHandle(projectId, projectPath) {
  const questionsPath = path.join(projectPath, 'questions.json');
  let questionList = [];
  try {
    const questionsData = await readJsonFile(questionsPath);
    if (questionsData.length === 0) return;
    for (const question of questionsData) {
      // 确保 chunk 已存在
      let chunk = await db.chunks.findFirst({ where: { name: question.chunkId, projectId } });
      if (!chunk) {
        console.error(`Chunk with name ${question.chunkId} not found for project ${projectPath}`);
        continue;
      }
      if (!question.questions || question.questions.length === 0) continue;
      for (const item of question.questions) {
        const questionData = {
          projectId: projectId,
          chunkId: chunk.id,
          question: item.question,
          label: item.label
        };
        questionList.push(questionData);
      }
    }
    return await db.questions.createMany({ data: questionList });
  } catch (error) {
    console.error('Error questions.json insert db:', error);
    throw error;
  }
}

//数据集文件处理
async function datasetHandle(projectId, projectPath) {
  const datasetsPath = path.join(projectPath, 'datasets.json');
  let datasetList = [];
  try {
    const datasetsData = await readJsonFile(datasetsPath);
    if (datasetsData.length === 0) return;
    for (const dataset of datasetsData) {
      let chunk = await db.chunks.findFirst({ where: { name: dataset.chunkId, projectId } });
      if (!chunk) {
        console.error(`Chunk with name ${dataset.chunkId} not found for project ${projectPath}`);
        continue;
      }
      let question = await db.questions.findFirst({ where: { question: dataset.question } });
      if (!question) {
        console.error(`Question with name ${dataset.question} not found for project ${projectPath}`);
        continue;
      }
      const datasetData = {
        projectId: projectId,
        chunkName: chunk.name,
        chunkContent: '',
        questionId: question.id,
        question: dataset.question,
        answer: dataset.answer,
        model: dataset.model,
        questionLabel: dataset.questionLabel,
        createAt: dataset.createdAt,
        cot: dataset.cot ? dataset.cot : '',
        confirmed: dataset.confirmed ? dataset.confirmed : false
      };
      datasetList.push(datasetData);
    }
    return await db.datasets.createMany({ data: datasetList });
  } catch (error) {
    console.error('Error datasets.json insert db:', error);
    throw error;
  }
}

//批量更新问题的答案状态
async function updateQuestions(projectId) {
  const result = await db.$queryRaw`
    UPDATE Questions
    SET answered = 1
    WHERE EXISTS (
      SELECT 1
      FROM Datasets d
      WHERE d.question = Questions.question
        AND Questions.projectId = ${projectId}
    )
  `;

  console.log(result);
}

// 复制文件夹
async function copyDirRecursive(src, dest, projectNewId) {
  try {
    // 检查源路径是否存在
    if (!fs.existsSync(src)) {
      console.error(`Source directory not found: ${src}`);
      return;
    }

    // 确保目标路径存在
    if (!fs.existsSync(dest)) {
      fs.mkdirSync(dest, { recursive: true });
    }

    // 读取源路径下的所有文件和子目录
    const entries = await fs.promises.readdir(src, { withFileTypes: true });

    let old = ['config.json', 'chunks', 'questions.json', 'datasets.json', 'model-config.json'];
    for (const entry of entries) {
      if (old.includes(entry.name)) {
        continue;
      }
      const srcPath = path.join(src, entry.name);
      const destPath = path.join(dest, entry.name);

      if (entry.isDirectory()) {
        // 如果是目录，递归复制
        await copyDirRecursive(srcPath, destPath, projectNewId);
      } else {
        // 如果是文件，直接复制
        await fs.promises.copyFile(srcPath, destPath);
      }
    }
  } catch (error) {
    console.error(`Failed to copy directory from ${src} to ${dest}`, error);
  }
}

async function safeReadDir(dirPath, options = {}) {
  try {
    if (fs.existsSync(dirPath)) {
      return await fs.promises.readdir(dirPath, options);
    }
    return [];
  } catch (error) {
    console.error(`Error reading directory: ${dirPath}`, error);
    return [];
  }
}

import { NextResponse } from 'next/server';
import { getProject } from '@/lib/db/projects';
import path from 'path';
import { getProjectRoot, ensureDir } from '@/lib/db/base';
import { promises as fs } from 'fs';
import {
  checkUploadFileInfoByMD5,
  createUploadFileInfo,
  delUploadFileInfoById,
  getUploadFilesPagination
} from '@/lib/db/upload-files';
import { getFileMD5 } from '@/lib/util/file';
import { batchSaveTags } from '@/lib/db/tags';
import { getProjectChunks, getProjectTocByName } from '@/lib/file/text-splitter';
import { handleDomainTree } from '@/lib/util/domain-tree';

// Replace the deprecated config export with the new export syntax
export const dynamic = 'force-dynamic';
// This tells Next.js not to parse the request body automatically
export const bodyParser = false;

// 获取项目文件列表
export async function GET(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
    }
    const { searchParams } = new URL(request.url);
    const page = parseInt(searchParams.get('page')) || 1;
    const pageSize = parseInt(searchParams.get('pageSize')) || 10; // 每页10个文件，支持分页
    const fileName = searchParams.get('fileName') || '';
    const getAllIds = searchParams.get('getAllIds') === 'true'; // 新增：获取所有文件ID的标志

    // 如果请求所有文件ID，直接返回ID列表
    if (getAllIds) {
      const allFiles = await getUploadFilesPagination(projectId, 1, 9999, fileName); // 获取所有文件
      const allFileIds = allFiles.data?.map(file => String(file.id)) || [];
      return NextResponse.json({ allFileIds });
    }
    // 获取文件列表
    const files = await getUploadFilesPagination(projectId, page, pageSize, fileName);

    return NextResponse.json(files);
  } catch (error) {
    console.error('Error obtaining file list:', String(error));
    return NextResponse.json({ error: error.message || 'Error obtaining file list' }, { status: 500 });
  }
}

// 删除文件
export async function DELETE(request, { params }) {
  try {
    const { projectId } = params;
    const { searchParams } = new URL(request.url);
    const fileId = searchParams.get('fileId');
    const domainTreeAction = searchParams.get('domainTreeAction') || 'keep';

    // 从请求体中获取模型信息和语言环境
    let model, language;
    try {
      const requestData = await request.json();
      model = requestData.model;
      language = requestData.language || '中文';
    } catch (error) {
      console.warn('解析请求体失败，使用默认值:', error);
      // 如果无法解析请求体，使用默认值
      model = {
        providerId: 'openai',
        modelName: 'gpt-3.5-turbo',
        apiKey: process.env.OPENAI_API_KEY || ''
      };
      language = '中文';
    }

    // 验证项目ID和文件名
    if (!projectId) {
      return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
    }

    if (!fileId) {
      return NextResponse.json({ error: 'The file name cannot be empty' }, { status: 400 });
    }

    // 获取项目信息
    const project = await getProject(projectId);
    if (!project) {
      return NextResponse.json({ error: 'The project does not exist' }, { status: 404 });
    }

    // 删除文件及其相关的文本块、问题和数据集
    const { stats, fileName, fileInfo } = await delUploadFileInfoById(fileId);

    try {
      const projectRoot = await getProjectRoot();
      const projectPath = path.join(projectRoot, projectId);
      const tocDir = path.join(projectPath, 'toc');
      const baseName = path.basename(fileInfo.fileName, path.extname(fileInfo.fileName));
      const tocPath = path.join(tocDir, `${baseName}-toc.json`);

      // 检查文件是否存在再删除
      await fs.unlink(tocPath);
      console.log(`成功删除 TOC 文件: ${tocPath}`);
    } catch (error) {
      console.error(`删除 TOC 文件失败:`, String(error));
      // 即使 TOC 文件删除失败，不影响整体结果
    }

    // 如果选择了保持领域树不变，直接返回删除结果
    if (domainTreeAction === 'keep') {
      return NextResponse.json({
        message: '文件删除成功',
        stats: stats,
        domainTreeAction: 'keep',
        cascadeDelete: true
      });
    }

    // 处理领域树更新
    try {
      // 获取项目的所有文件
      const { chunks, toc } = await getProjectChunks(projectId);

      // 如果不存在文本块，说明项目已经没有文件了
      if (!chunks || chunks.length === 0) {
        // 清空领域树
        await batchSaveTags(projectId, []);
        return NextResponse.json({
          message: '文件删除成功，领域树已清空',
          stats: stats,
          domainTreeAction,
          cascadeDelete: true
        });
      }

      const deleteToc = await getProjectTocByName(projectId, fileName);

      // 调用领域树处理模块
      await handleDomainTree({
        projectId,
        action: domainTreeAction,
        allToc: toc,
        model,
        language,
        deleteToc,
        project
      });
    } catch (error) {
      console.error('Error updating domain tree after file deletion:', String(error));
      // 即使领域树更新失败，也不影响文件删除的结果
    }

    return NextResponse.json({
      message: '文件删除成功',
      stats: stats,
      domainTreeAction,
      cascadeDelete: true
    });
  } catch (error) {
    console.error('Error deleting file:', String(error));
    return NextResponse.json({ error: error.message || 'Error deleting file' }, { status: 500 });
  }
}

// 上传文件
export async function POST(request, { params }) {
  console.log('File upload request processing, parameters:', params);
  const { projectId } = params;

  // 验证项目ID
  if (!projectId) {
    console.log('The project ID cannot be empty, returning 400 error');
    return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
  }

  // 获取项目信息
  const project = await getProject(projectId);
  if (!project) {
    console.log('The project does not exist, returning 404 error');
    return NextResponse.json({ error: 'The project does not exist' }, { status: 404 });
  }
  console.log('Project information retrieved successfully:', project.name || project.id);

  try {
    console.log('Try using alternate methods for file upload...');

    // 检查请求头中是否包含文件名
    const encodedFileName = request.headers.get('x-file-name');
    const fileName = encodedFileName ? decodeURIComponent(encodedFileName) : null;
    console.log('Get file name from request header:', fileName);

    if (!fileName) {
      console.log('The request header does not contain a file name');
      return NextResponse.json(
        { error: 'The request header does not contain a file name (x-file-name)' },
        { status: 400 }
      );
    }

    // 检查文件类型
    if (!fileName.endsWith('.md') && !fileName.endsWith('.pdf')) {
      return NextResponse.json({ error: 'Only Markdown files are supported' }, { status: 400 });
    }

    // 直接从请求体中读取二进制数据
    const fileBuffer = Buffer.from(await request.arrayBuffer());

    // 保存文件
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);
    const filesDir = path.join(projectPath, 'files');

    await ensureDir(filesDir);

    const filePath = path.join(filesDir, fileName);
    await fs.writeFile(filePath, fileBuffer);
    //获取文件大小
    const stats = await fs.stat(filePath);
    //获取文件md5
    const md5 = await getFileMD5(filePath);
    //获取文件扩展名
    const ext = path.extname(filePath);

    // let res = await checkUploadFileInfoByMD5(projectId, md5);
    // if (res) {
    //   return NextResponse.json({ error: `【${fileName}】该文件已在此项目中存在` }, { status: 400 });
    // }

    let fileInfo = await createUploadFileInfo({
      projectId,
      fileName,
      size: stats.size,
      md5,
      fileExt: ext,
      path: filesDir
    });

    console.log('The file upload process is complete, and a successful response is returned');
    return NextResponse.json({
      message: 'File uploaded successfully',
      fileName,
      filePath,
      fileId: fileInfo.id
    });
  } catch (error) {
    console.error('Error processing file upload:', String(error));
    console.error('Error stack:', error.stack);
    return NextResponse.json(
      {
        error: 'File upload failed: ' + (error.message || 'Unknown error')
      },
      { status: 500 }
    );
  }
}

'use client';

import axios from 'axios';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
  Container,
  Box,
  Tabs,
  Tab,
  IconButton,
  Collapse,
  Dialog,
  DialogContent,
  DialogTitle,
  Typography
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import FullscreenIcon from '@mui/icons-material/Fullscreen';
import CloseIcon from '@mui/icons-material/Close';
import FileUploader from '@/components/text-split/FileUploader';
import FileList from '@/components/text-split/components/FileList';
import DeleteConfirmDialog from '@/components/text-split/components/DeleteConfirmDialog';
import LoadingBackdrop from '@/components/text-split/LoadingBackdrop';
import PdfSettings from '@/components/text-split/PdfSettings';
import ChunkList from '@/components/text-split/ChunkList';
import DomainAnalysis from '@/components/text-split/DomainAnalysis';
import useTaskSettings from '@/hooks/useTaskSettings';
import { useAtomValue } from 'jotai/index';
import { selectedModelInfoAtom } from '@/lib/store';
import useChunks from './useChunks';
import useQuestionGeneration from './useQuestionGeneration';
import useFileProcessing from './useFileProcessing';
import useFileProcessingStatus from '@/hooks/useFileProcessingStatus';
import { toast } from 'sonner';

export default function TextSplitPage({ params }) {
  const { t } = useTranslation();
  const theme = useTheme();
  const { projectId } = params;
  const [activeTab, setActiveTab] = useState(0);
  const { taskSettings } = useTaskSettings(projectId);
  const [pdfStrategy, setPdfStrategy] = useState('default');
  const [questionFilter, setQuestionFilter] = useState('all'); // 'all', 'generated', 'ungenerated'
  const [selectedViosnModel, setSelectedViosnModel] = useState('');
  const selectedModelInfo = useAtomValue(selectedModelInfoAtom);
  const { taskFileProcessing, task } = useFileProcessingStatus();
  const [currentPage, setCurrentPage] = useState(1);
  const [uploadedFiles, setUploadedFiles] = useState({ data: [], total: 0 });
  const [searchFileName, setSearchFileName] = useState('');

  // 上传区域的展开/折叠状态
  const [uploaderExpanded, setUploaderExpanded] = useState(true);

  // 文献列表(FileList)展示对话框状态
  const [fileListDialogOpen, setFileListDialogOpen] = useState(false);

  // 使用自定义hooks
  const { chunks, tocData, loading, fetchChunks, handleDeleteChunk, handleEditChunk, updateChunks, setLoading } =
    useChunks(projectId, questionFilter);

  // 获取文件列表
  const fetchUploadedFiles = async (page = currentPage, fileName = searchFileName) => {
    try {
      setLoading(true);
      const params = new URLSearchParams({
        page: page.toString(),
        size: '10'
      });

      if (fileName && fileName.trim()) {
        params.append('fileName', fileName.trim());
      }

      const response = await axios.get(`/api/projects/${projectId}/files?${params}`);
      setUploadedFiles(response.data);
    } catch (error) {
      console.error('Error fetching files:', error);
      toast.error(error.message || '获取文件列表失败');
    } finally {
      setLoading(false);
    }
  };

  // 删除文件确认对话框状态
  const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
  const [fileToDelete, setFileToDelete] = useState(null);

  // 打开删除确认对话框
  const openDeleteConfirm = (fileId, fileName) => {
    setFileToDelete({ fileId, fileName });
    setDeleteConfirmOpen(true);
  };

  // 关闭删除确认对话框
  const closeDeleteConfirm = () => {
    setDeleteConfirmOpen(false);
    setFileToDelete(null);
  };

  // 确认删除文件
  const confirmDeleteFile = async () => {
    if (!fileToDelete) return;

    try {
      setLoading(true);
      closeDeleteConfirm();

      await axios.delete(`/api/projects/${projectId}/files/${fileToDelete.fileId}`);
      await fetchUploadedFiles();
      fetchChunks();

      toast.success(
        t('textSplit.deleteSuccess', { fileName: fileToDelete.fileName }) || `删除 ${fileToDelete.fileName} 成功`
      );
    } catch (error) {
      console.error('删除文件出错:', error);
      toast.error(error.message || '删除文件失败');
    } finally {
      setLoading(false);
      setFileToDelete(null);
    }
  };

  const {
    processing,
    progress: questionProgress,
    handleGenerateQuestions
  } = useQuestionGeneration(projectId, taskSettings);

  const { fileProcessing, progress: pdfProgress, handleFileProcessing } = useFileProcessing(projectId);

  // 当前页面使用的进度状态
  const progress = processing ? questionProgress : pdfProgress;

  // 加载文本块数据和文件列表
  useEffect(() => {
    fetchChunks('all');
    fetchUploadedFiles();
  }, [fetchChunks, taskFileProcessing, currentPage, searchFileName]);

  /**
   * 对上传后的文件进行处理
   */
  const handleUploadSuccess = async (fileNames, pdfFiles, domainTreeAction) => {
    try {
      await handleFileProcessing(fileNames, pdfStrategy, selectedViosnModel, domainTreeAction);
      location.reload();
    } catch (error) {
      toast.error('File upload failed' + error.message || '');
    }
  };

  // 包装生成问题的处理函数
  const onGenerateQuestions = async chunkIds => {
    await handleGenerateQuestions(chunkIds, selectedModelInfo, fetchChunks);
  };

  useEffect(() => {
    const url = new URL(window.location.href);
    if (questionFilter !== 'all') {
      url.searchParams.set('filter', questionFilter);
    } else {
      url.searchParams.delete('filter');
    }
    window.history.replaceState({}, '', url);
    fetchChunks(questionFilter);
  }, [questionFilter]);

  const handleSelected = array => {
    if (array.length > 0) {
      axios.post(`/api/projects/${projectId}/chunks`, { array }).then(response => {
        updateChunks(response.data);
      });
    } else {
      fetchChunks();
    }
  };

  return (
    <Container maxWidth="lg" sx={{ mt: 4, mb: 8, position: 'relative' }}>
      {/* 文件上传组件 */}

      <Box
        sx={{ position: 'absolute', top: -18, left: '50%', transform: 'translateX(-50%)', zIndex: 1, display: 'flex' }}
      >
        <IconButton
          onClick={() => setUploaderExpanded(!uploaderExpanded)}
          sx={{
            bgcolor: 'background.paper',
            boxShadow: 1,
            mr: uploaderExpanded ? 1 : 0 // 展开时按钮之间留点间距
          }}
          size="small"
        >
          {uploaderExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
        </IconButton>

        {/* 文献列表扩展按钮，仅在上部区域展开时显示 */}
        {uploaderExpanded && (
          <IconButton
            color="primary"
            onClick={() => setFileListDialogOpen(true)}
            sx={{ bgcolor: 'background.paper', boxShadow: 1 }}
            size="small"
            title={t('textSplit.expandFileList') || '扩展文献列表'}
          >
            <FullscreenIcon />
          </IconButton>
        )}
      </Box>

      <Collapse in={uploaderExpanded}>
        <FileUploader
          projectId={projectId}
          onUploadSuccess={handleUploadSuccess}
          onFileDeleted={fetchChunks}
          setPageLoading={setLoading}
          sendToPages={handleSelected}
          setPdfStrategy={setPdfStrategy}
          pdfStrategy={pdfStrategy}
          selectedViosnModel={selectedViosnModel}
          setSelectedViosnModel={setSelectedViosnModel}
          taskFileProcessing={taskFileProcessing}
          fileTask={task}
        >
          <PdfSettings
            pdfStrategy={pdfStrategy}
            setPdfStrategy={setPdfStrategy}
            selectedViosnModel={selectedViosnModel}
            setSelectedViosnModel={setSelectedViosnModel}
          />
        </FileUploader>
      </Collapse>

      {/* 标签页 */}
      <Box sx={{ width: '100%', mb: 3 }}>
        <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
          <Tabs
            value={activeTab}
            onChange={(event, newValue) => {
              setActiveTab(newValue);
            }}
            variant="fullWidth"
            sx={{ borderBottom: 1, borderColor: 'divider', flexGrow: 1 }}
          >
            <Tab label={t('textSplit.tabs.smartSplit')} />
            <Tab label={t('textSplit.tabs.domainAnalysis')} />
          </Tabs>
        </Box>

        {/* 智能分割标签内容 */}
        {activeTab === 0 && (
          <ChunkList
            projectId={projectId}
            chunks={chunks}
            onDelete={handleDeleteChunk}
            onEdit={handleEditChunk}
            onGenerateQuestions={onGenerateQuestions}
            loading={loading}
            questionFilter={questionFilter}
            setQuestionFilter={setQuestionFilter}
            selectedModel={selectedModelInfo}
          />
        )}

        {/* 领域分析标签内容 */}
        {activeTab === 1 && <DomainAnalysis projectId={projectId} toc={tocData} loading={loading} />}
      </Box>

      {/* 加载中蒙版 */}
      <LoadingBackdrop open={loading} title={t('textSplit.loading')} description={t('textSplit.fetchingDocuments')} />

      {/* 处理中蒙版 */}
      <LoadingBackdrop open={processing} title={t('textSplit.processing')} progress={progress} />

      {/* 文件处理进度蒙版 */}
      <LoadingBackdrop open={fileProcessing} title={t('textSplit.pdfProcessing')} progress={progress} />

      {/* 文件删除确认对话框 */}
      <DeleteConfirmDialog
        open={deleteConfirmOpen}
        fileName={fileToDelete?.fileName}
        onClose={closeDeleteConfirm}
        onConfirm={confirmDeleteFile}
      />

      {/* 文献列表对话框 */}
      <Dialog
        open={fileListDialogOpen}
        onClose={() => setFileListDialogOpen(false)}
        maxWidth="lg"
        fullWidth
        sx={{ '& .MuiDialog-paper': { bgcolor: 'background.default' } }}
      >
        <DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', px: 3, py: 1 }}>
          <Typography variant="h6">{t('textSplit.fileList')}</Typography>
          <IconButton edge="end" color="inherit" onClick={() => setFileListDialogOpen(false)} aria-label="close">
            <CloseIcon />
          </IconButton>
        </DialogTitle>
        <DialogContent dividers sx={{ p: 3 }}>
          {/* 此处复用 FileUploader 组件中的 FileList 部分 */}
          <Box sx={{ minHeight: '80vh' }}>
            {/* 文件列表内容 */}
            <FileList
              theme={theme}
              files={uploadedFiles}
              loading={loading}
              setPageLoading={setLoading}
              sendToFileUploader={array => handleSelected(array)}
              onDeleteFile={(fileId, fileName) => openDeleteConfirm(fileId, fileName)}
              projectId={projectId}
              currentPage={currentPage}
              onPageChange={(page, fileName) => {
                if (fileName !== undefined) {
                  // 搜索时更新搜索关键词和页码
                  setSearchFileName(fileName);
                  setCurrentPage(page);
                } else {
                  // 翻页时只更新页码
                  setCurrentPage(page);
                }
              }}
              isFullscreen={true} // 在对话框中移除高度限制
            />
          </Box>
        </DialogContent>
      </Dialog>
    </Container>
  );
}

'use client';

import { useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { selectedModelInfoAtom } from '@/lib/store';
import { useAtomValue } from 'jotai/index';
import { toast } from 'sonner';
import i18n from '@/lib/i18n';
import axios from 'axios';

/**
 * 文件处理的自定义Hook
 * @param {string} projectId - 项目ID
 * @returns {Object} - 文件处理状态和操作方法
 */
export default function useFileProcessing(projectId) {
  const { t } = useTranslation();
  const [fileProcessing, setFileProcessing] = useState(false);
  const [progress, setProgress] = useState({
    total: 0,
    completed: 0,
    percentage: 0,
    questionCount: 0
  });
  const model = useAtomValue(selectedModelInfoAtom);

  /**
   * 重置进度状态
   */
  const resetProgress = useCallback(() => {
    setTimeout(() => {
      setProgress({
        total: 0,
        completed: 0,
        percentage: 0,
        questionCount: 0
      });
    }, 1000); // 延迟重置，让用户看到完成的进度
  }, []);

  /**
   * 处理文件
   * @param {Array} files - 文件列表
   * @param {string} pdfStrategy - PDF处理策略
   * @param {string} selectedViosnModel - 选定的视觉模型
   */
  const handleFileProcessing = useCallback(
    async (files, pdfStrategy, selectedViosnModel, domainTreeAction) => {
      try {
        const currentLanguage = i18n.language === 'zh-CN' ? '中文' : 'en';

        //获取到视觉策略要使用的模型
        const availableModels = JSON.parse(localStorage.getItem('modelConfigList'));
        const vsionModel = availableModels.find(m => m.id === selectedViosnModel);

        const response = await axios.post(`/api/projects/${projectId}/tasks`, {
          taskType: 'file-processing',
          modelInfo: localStorage.getItem('selectedModelInfo'),
          language: currentLanguage,
          detail: '文件处理任务',
          note: {
            vsionModel,
            projectId,
            fileList: files,
            strategy: pdfStrategy,
            domainTreeAction
          }
        });

        if (response.data?.code !== 0) {
          throw new Error(t('textSplit.pdfProcessingFailed') + (response.data?.error || ''));
        }

        //提示后台任务进行中
        toast.success(t('textSplit.pdfProcessingToast'));
      } catch (error) {
        toast.error(t('textSplit.pdfProcessingFailed') + error.message || '');
      }
    },
    [projectId, t, resetProgress]
  );

  return {
    fileProcessing,
    progress,
    setFileProcessing,
    setProgress,
    handleFileProcessing,
    resetProgress
  };
}

import { NextResponse } from 'next/server';
import { main } from '@/lib/db/fileToDb';

// 存储迁移任务状态
const migrationTasks = new Map();

/**
 * 开始迁移任务
 */
export async function POST() {
  try {
    // 生成唯一的任务ID
    const taskId = Date.now().toString();

    // 初始化任务状态
    migrationTasks.set(taskId, {
      status: 'running',
      progress: 0,
      total: 0,
      completed: 0,
      error: null,
      startTime: Date.now()
    });

    // 异步执行迁移任务
    executeMigration(taskId);

    // 返回任务ID
    return NextResponse.json({
      success: true,
      taskId
    });
  } catch (error) {
    console.error('启动迁移任务失败:', String(error));
    return NextResponse.json(
      {
        success: false,
        error: error.message
      },
      { status: 500 }
    );
  }
}

/**
 * 获取迁移任务状态
 */
export async function GET(request) {
  try {
    // 从URL获取任务ID
    const { searchParams } = new URL(request.url);
    const taskId = searchParams.get('taskId');

    if (!taskId) {
      return NextResponse.json(
        {
          success: false,
          error: '缺少任务ID'
        },
        { status: 400 }
      );
    }

    // 获取任务状态
    const task = migrationTasks.get(taskId);

    if (!task) {
      return NextResponse.json(
        {
          success: false,
          error: '任务不存在'
        },
        { status: 404 }
      );
    }

    // 返回任务状态
    return NextResponse.json({
      success: true,
      task
    });
  } catch (error) {
    console.error('获取迁移任务状态失败:', String(error));
    return NextResponse.json(
      {
        success: false,
        error: error.message
      },
      { status: 500 }
    );
  }
}

/**
 * 异步执行迁移任务
 * @param {string} taskId 任务ID
 */
async function executeMigration(taskId) {
  try {
    // 获取任务状态
    const task = migrationTasks.get(taskId);

    if (!task) {
      console.error(`任务 ${taskId} 不存在`);
      return;
    }

    // 更新任务状态为运行中
    task.status = 'running';
    task.progress = 0;
    task.completed = 0;
    task.total = 0;
    task.startTime = Date.now();

    // 每秒更新一次任务状态到存储中，以便前端能获取最新进度
    const statusUpdateInterval = setInterval(() => {
      // 只在任务还在运行时更新
      if (task.status === 'running') {
        migrationTasks.set(taskId, { ...task });
        console.log(`更新任务状态: ${taskId}, 进度: ${task.progress}%, 完成: ${task.completed}/${task.total}`);
      } else {
        // 如果任务已经结束，停止定时更新
        clearInterval(statusUpdateInterval);
      }
    }, 1000);

    // 执行迁移操作
    // 将任务状态对象传递给main函数，以便实时更新进度
    const count = await main(task);

    // 清除状态更新定时器
    clearInterval(statusUpdateInterval);

    // 再次确保任务状态为完成
    task.status = 'completed';
    task.progress = 100;
    task.completed = count;
    if (task.total === 0) task.total = count; // 确保总数不为零
    task.endTime = Date.now();

    // 更新最终任务状态
    migrationTasks.set(taskId, { ...task });

    // 任务完成后，设置一个定时器清理任务状态（例如30分钟后）
    setTimeout(
      () => {
        migrationTasks.delete(taskId);
        console.log(`清理任务状态: ${taskId}`);
      },
      30 * 60 * 1000
    );
  } catch (error) {
    console.error(`执行迁移任务 ${taskId} 失败:`, String(error));

    // 获取任务状态
    const task = migrationTasks.get(taskId);

    if (task) {
      // 更新任务状态为失败
      task.status = 'failed';
      task.error = error.message;
      task.endTime = Date.now();

      // 更新任务状态
      migrationTasks.set(taskId, task);
    }
  }
}

'use client';

import { useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import i18n from '@/lib/i18n';
import request from '@/lib/util/request';
import { processInParallel } from '@/lib/util/async';
import { toast } from 'sonner';

/**
 * 问题生成的自定义Hook
 * @param {string} projectId - 项目ID
 * @param {Function} onError - 错误处理回调
 * @param {Object} taskSettings - 任务设置
 * @returns {Object} - 问题生成状态和操作方法
 */
export default function useQuestionGeneration(projectId, taskSettings) {
  const { t } = useTranslation();
  const [processing, setProcessing] = useState(false);
  const [progress, setProgress] = useState({
    total: 0,
    completed: 0,
    percentage: 0,
    questionCount: 0
  });

  /**
   * 重置进度状态
   */
  const resetProgress = useCallback(() => {
    setTimeout(() => {
      setProgress({
        total: 0,
        completed: 0,
        percentage: 0,
        questionCount: 0
      });
    }, 1000); // 延迟重置，让用户看到完成的进度
  }, []);

  /**
   * 处理生成问题
   * @param {Array} chunkIds - 文本块ID列表
   * @param {Object} selectedModelInfo - 选定的模型信息
   * @param {Function} fetchChunks - 刷新文本块列表的函数
   */
  const handleGenerateQuestions = useCallback(
    async (chunkIds, selectedModelInfo, fetchChunks) => {
      try {
        setProcessing(true);
        // 重置进度状态
        setProgress({
          total: chunkIds.length,
          completed: 0,
          percentage: 0,
          questionCount: 0
        });

        let model = selectedModelInfo;

        // 如果仍然没有模型信息，抛出错误
        if (!model) {
          throw new Error(t('textSplit.selectModelFirst'));
        }

        // 如果是单个文本块，直接调用单个生成接口
        if (chunkIds.length === 1) {
          const chunkId = chunkIds[0];
          // 获取当前语言环境
          const currentLanguage = i18n.language === 'zh-CN' ? '中文' : 'en';

          const response = await request(`/api/projects/${projectId}/chunks/${chunkId}/questions`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              model,
              language: currentLanguage,
              enableGaExpansion: true // 默认启用GA扩展
            })
          });

          if (!response.ok) {
            const errorData = await response.json();
            throw new Error(errorData.error || t('textSplit.generateQuestionsFailed', { chunkId }));
          }

          const data = await response.json();
          toast.success(
            t('textSplit.questionsGeneratedSuccess', {
              total: data.total
            }),
            {
              duration: 3000
            }
          );
        } else {
          // 如果是多个文本块，循环调用单个文本块的问题生成接口
          let totalQuestions = 0;
          let successCount = 0;
          let errorCount = 0;

          // 单个文本块处理函数
          const processChunk = async chunkId => {
            try {
              // 获取当前语言环境
              const currentLanguage = i18n.language === 'zh-CN' ? '中文' : 'en';

              const response = await request(
                `/api/projects/${projectId}/chunks/${encodeURIComponent(chunkId)}/questions`,
                {
                  method: 'POST',
                  headers: {
                    'Content-Type': 'application/json'
                  },
                  body: JSON.stringify({
                    model,
                    language: currentLanguage,
                    enableGaExpansion: true // 默认启用GA扩展
                  })
                }
              );

              if (!response.ok) {
                const errorData = await response.json();
                console.error(t('textSplit.generateQuestionsForChunkFailed', { chunkId }), errorData.error);
                errorCount++;
                return { success: false, chunkId, error: errorData.error };
              }

              const data = await response.json();
              console.log(t('textSplit.questionsGenerated', { chunkId, total: data.total }));

              // 更新进度状态
              setProgress(prev => {
                const completed = prev.completed + 1;
                const percentage = Math.round((completed / prev.total) * 100);
                const questionCount = prev.questionCount + (data.total || 0);

                return {
                  ...prev,
                  completed,
                  percentage,
                  questionCount
                };
              });

              totalQuestions += data.total || 0;
              successCount++;
              return { success: true, chunkId, total: data.total };
            } catch (error) {
              console.error(t('textSplit.generateQuestionsForChunkError', { chunkId }), error);
              errorCount++;

              // 更新进度状态（即使失败也计入已处理）
              setProgress(prev => {
                const completed = prev.completed + 1;
                const percentage = Math.round((completed / prev.total) * 100);

                return {
                  ...prev,
                  completed,
                  percentage
                };
              });

              return { success: false, chunkId, error: error.message };
            }
          };

          // 并行处理所有文本块，使用任务设置中的并发限制
          await processInParallel(chunkIds, processChunk, taskSettings?.concurrencyLimit || 2);

          // 处理完成后设置结果消息
          if (errorCount > 0) {
            toast.warning(
              t('textSplit.partialSuccess', {
                successCount,
                total: chunkIds.length,
                errorCount
              })
            );
          } else {
            toast.success(
              t('textSplit.allSuccess', {
                successCount,
                totalQuestions
              })
            );
          }
        }

        // 刷新文本块列表
        if (fetchChunks) {
          fetchChunks();
        }
      } catch (error) {
        toast.error(error.message);
      } finally {
        setProcessing(false);
        // 重置进度状态
        resetProgress();
      }
    },
    [projectId, t, resetProgress, taskSettings]
  );

  return {
    processing,
    progress,
    setProgress,
    setProcessing,
    handleGenerateQuestions,
    resetProgress
  };
}

import { NextResponse } from 'next/server';
import { batchGenerateGaPairs } from '@/lib/services/ga/ga-pairs';
import { getUploadFileInfoById } from '@/lib/db/upload-files'; // 导入单个文件查询函数

/**
 * 批量生成多个文件的 GA 对
 */
export async function POST(request, { params }) {
  try {
    const { projectId } = params;
    const body = await request.json();

    if (!projectId) {
      return NextResponse.json({ error: 'Project ID is required' }, { status: 400 });
    }

    const { fileIds, modelConfigId, language = '中文', appendMode = false } = body;

    if (!fileIds || !Array.isArray(fileIds) || fileIds.length === 0) {
      return NextResponse.json({ error: 'File IDs array is required' }, { status: 400 });
    }

    if (!modelConfigId) {
      return NextResponse.json({ error: 'Model configuration ID is required' }, { status: 400 });
    }

    console.log('开始处理批量生成GA对请求');
    console.log('项目ID:', projectId);
    console.log('请求的文件IDs:', fileIds);

    // 使用 getUploadFileInfoById 逐个验证文件
    const validFiles = [];
    const invalidFileIds = [];

    for (const fileId of fileIds) {
      try {
        console.log(`正在验证文件: ${fileId}`);
        const fileInfo = await getUploadFileInfoById(fileId);

        if (fileInfo && fileInfo.projectId === projectId) {
          console.log(`文件验证成功: ${fileInfo.fileName}`);
          validFiles.push(fileInfo);
        } else if (fileInfo) {
          console.log(`文件属于其他项目: ${fileInfo.projectId} != ${projectId}`);
          invalidFileIds.push(fileId);
        } else {
          console.log(`文件不存在: ${fileId}`);
          invalidFileIds.push(fileId);
        }
      } catch (error) {
        console.error(`验证文件 ${fileId} 时出错:`, String(error));
        invalidFileIds.push(fileId);
      }
    }

    console.log(`文件验证完成: 有效${validFiles.length}个, 无效${invalidFileIds.length}个`);

    if (validFiles.length === 0) {
      return NextResponse.json(
        {
          error: 'No valid files found',
          debug: {
            projectId,
            requestedIds: fileIds,
            invalidIds: invalidFileIds,
            message: 'None of the requested files belong to this project or exist in the database'
          }
        },
        { status: 404 }
      );
    }

    // 批量生成 GA 对
    console.log('开始批量生成GA对...');
    console.log('追加模式:', appendMode);
    const results = await batchGenerateGaPairs(
      projectId,
      validFiles,
      modelConfigId,
      language,
      appendMode // 传递追加模式参数
    );

    // 统计结果
    const successCount = results.filter(r => r.success).length;
    const failureCount = results.filter(r => !r.success).length;

    console.log(`批量生成完成: 成功${successCount}个, 失败${failureCount}个`);

    return NextResponse.json({
      success: true,
      data: results,
      summary: {
        total: results.length,
        success: successCount,
        failure: failureCount,
        processed: validFiles.length,
        skipped: invalidFileIds.length
      },
      message: `Generated GA pairs for ${successCount} files, ${failureCount} failed, ${invalidFileIds.length} files not found`
    });
  } catch (error) {
    console.error('Error batch generating GA pairs:', String(error));
    return NextResponse.json({ error: String(error) || 'Failed to batch generate GA pairs' }, { status: 500 });
  }
}

'use client';

import React from 'react';
import {
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableHead,
  TableRow,
  Paper,
  Typography,
  CircularProgress,
  Box,
  TablePagination
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { formatDistanceToNow } from 'date-fns';
import { zhCN, enUS } from 'date-fns/locale';

// 导入子组件
import TaskStatusChip from './TaskStatusChip';
import TaskProgress from './TaskProgress';
import TaskActions from './TaskActions';

export default function TasksTable({
  tasks,
  loading,
  handleAbortTask,
  handleDeleteTask,
  page,
  rowsPerPage,
  handleChangePage,
  handleChangeRowsPerPage,
  totalCount
}) {
  const { t, i18n } = useTranslation();

  // 格式化日期
  const formatDate = dateString => {
    if (!dateString) return '-';
    const date = new Date(dateString);
    return formatDistanceToNow(date, {
      addSuffix: true,
      locale: i18n.language === 'zh-CN' ? zhCN : enUS
    });
  };

  // 计算任务运行时间
  const calculateDuration = (startTimeStr, endTimeStr) => {
    if (!startTimeStr || !endTimeStr) return '-';

    try {
      const startTime = new Date(startTimeStr);
      const endTime = new Date(endTimeStr);

      // 计算时间差（毫秒）
      const duration = endTime - startTime;

      // 将毫秒转换为人类可读格式
      const seconds = Math.floor(duration / 1000);

      if (seconds < 60) {
        return t('tasks.duration.seconds', { seconds });
      } else if (seconds < 3600) {
        const minutes = Math.floor(seconds / 60);
        const remainingSeconds = seconds % 60;
        return t('tasks.duration.minutes', { minutes, seconds: remainingSeconds });
      } else {
        const hours = Math.floor(seconds / 3600);
        const remainingMinutes = Math.floor((seconds % 3600) / 60);
        return t('tasks.duration.hours', { hours, minutes: remainingMinutes });
      }
    } catch (error) {
      console.error('计算运行时间出错:', error);
      return '-';
    }
  };

  // 解析模型信息
  const parseModelInfo = modelInfoString => {
    let modelInfo = '';
    try {
      const parsedModel = JSON.parse(modelInfoString);
      modelInfo = parsedModel.modelName || parsedModel.name || '-';
    } catch (error) {
      modelInfo = modelInfoString || '-';
    }
    return modelInfo;
  };

  // 任务类型本地化
  const getLocalizedTaskType = taskType => {
    return t(`tasks.types.${taskType}`, { defaultValue: taskType });
  };

  return (
    <React.Fragment>
      <TableContainer component={Paper} elevation={1} sx={{ borderRadius: 2, mb: 2 }}>
        <Table>
          <TableHead>
            <TableRow>
              <TableCell>{t('tasks.table.type')}</TableCell>
              <TableCell>{t('tasks.table.status')}</TableCell>
              <TableCell>{t('tasks.table.progress')}</TableCell>
              <TableCell>{t('tasks.table.success')}</TableCell>
              <TableCell>{t('tasks.table.failed')}</TableCell>
              <TableCell>{t('tasks.table.createTime')}</TableCell>
              <TableCell>{t('tasks.table.duration')}</TableCell>
              <TableCell>{t('tasks.table.model')}</TableCell>
              <TableCell align="right">{t('tasks.table.actions')}</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {loading && tasks.length === 0 ? (
              <TableRow>
                <TableCell colSpan={10} align="center" sx={{ py: 6 }}>
                  <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
                    <CircularProgress size={40} />
                    <Typography variant="body2" sx={{ mt: 2 }}>
                      {t('tasks.loading')}
                    </Typography>
                  </Box>
                </TableCell>
              </TableRow>
            ) : tasks.length === 0 ? (
              <TableRow>
                <TableCell colSpan={10} align="center" sx={{ py: 6 }}>
                  <Typography variant="body1">{t('tasks.empty')}</Typography>
                </TableCell>
              </TableRow>
            ) : (
              tasks.map(task => (
                <TableRow key={task.id}>
                  <TableCell>{getLocalizedTaskType(task.taskType)}</TableCell>
                  <TableCell>
                    <TaskStatusChip status={task.status} />
                  </TableCell>
                  <TableCell>
                    <TaskProgress task={task} />
                  </TableCell>
                  <TableCell>{task.completedCount ? task.completedCount - (task.errorCount || 0) : 0}</TableCell>
                  <TableCell>{task.errorCount || 0}</TableCell>
                  <TableCell>{formatDate(task.createAt)}</TableCell>
                  <TableCell>{task.endTime ? calculateDuration(task.startTime, task.endTime) : '-'}</TableCell>
                  <TableCell>{parseModelInfo(task.modelInfo)}</TableCell>
                  <TableCell align="right">
                    <TaskActions task={task} onAbort={handleAbortTask} onDelete={handleDeleteTask} />
                  </TableCell>
                </TableRow>
              ))
            )}
          </TableBody>
        </Table>
      </TableContainer>

      {tasks.length > 0 && (
        <TablePagination
          component="div"
          count={totalCount}
          page={page}
          onPageChange={handleChangePage}
          rowsPerPage={rowsPerPage}
          onRowsPerPageChange={handleChangeRowsPerPage}
          rowsPerPageOptions={[5, 10, 25]}
          labelRowsPerPage={t('datasets.rowsPerPage')}
          labelDisplayedRows={({ from, to, count }) => {
            // 根据实际分页操作计算正确的from和to
            const calculatedFrom = page * rowsPerPage + 1;
            const calculatedTo = Math.min((page + 1) * rowsPerPage, count);
            return t('datasets.pagination', {
              from: calculatedFrom,
              to: calculatedTo,
              count
            });
          }}
        />
      )}
    </React.Fragment>
  );
}

import { generateGaPairs } from './ga-generation';
import { getModelById } from '../models';
import { saveGaPairs, getGaPairsByFileId } from '@/lib/db/ga-pairs';
import { getProjectFileContentById } from '@/lib/db/files';
import logger from '@/lib/util/logger';

/**
 * Batch generate GA pairs for multiple files
 * @param {string} projectId - Project ID
 * @param {Array} files - Array of file objects
 * @param {string} modelConfigId - Model configuration ID
 * @param {string} language - Language for generation (default: '中文')
 * @param {boolean} appendMode - Whether to append to existing GA pairs (default: false)
 * @returns {Promise<Array>} - Array of generation results
 */
export async function batchGenerateGaPairs(projectId, files, modelConfigId, language = '中文', appendMode = false) {
  try {
    logger.info(`Starting batch GA pairs generation for ${files.length} files`);

    // Get model configuration
    const modelConfig = await getModelById(modelConfigId);
    if (!modelConfig) {
      throw new Error('Model configuration not found');
    }

    const results = [];

    // Process each file
    for (const file of files) {
      try {
        logger.info(`Processing file: ${file.fileName}`);

        // Check if GA pairs already exist for this file
        const existingPairs = await getGaPairsByFileId(file.id);

        // 在非追加模式下，如果已存在GA对则跳过
        if (!appendMode && existingPairs && existingPairs.length > 0) {
          logger.info(`GA pairs already exist for file ${file.fileName}, skipping`);
          results.push({
            fileId: file.id,
            fileName: file.fileName,
            success: true,
            skipped: true,
            message: 'GA pairs already exist',
            gaPairs: existingPairs
          });
          continue;
        }
        // Get file content
        const fileContent = await getProjectFileContentById(projectId, file.id);
        if (!fileContent) {
          throw new Error('File content not found');
        }
        // Limit content length for processing (max 50,000 characters)
        const maxLength = 50000;
        const content = fileContent.length > maxLength ? fileContent.substring(0, maxLength) + '...' : fileContent;

        // Generate GA pairs
        const gaPairs = await generateGaPairs(content, projectId, language);

        // Save GA pairs to database
        const savedPairs = await saveGaPairsForFile(projectId, file.id, gaPairs, appendMode, existingPairs);

        results.push({
          fileId: file.id,
          fileName: file.fileName,
          success: true,
          skipped: false,
          message: `Generated ${gaPairs.length} GA pairs`,
          gaPairs: savedPairs
        });

        logger.info(`Successfully generated GA pairs for file: ${file.fileName}`);
      } catch (error) {
        logger.error(`Failed to generate GA pairs for file ${file.fileName}:`, error);
        results.push({
          fileId: file.id,
          fileName: file.fileName,
          success: false,
          skipped: false,
          error: error.message,
          message: `Failed: ${error.message}`
        });
      }
    }

    logger.info(
      `Batch GA pairs generation completed. Success: ${results.filter(r => r.success).length}, Failed: ${results.filter(r => !r.success).length}`
    );
    return results;
  } catch (error) {
    logger.error('Batch GA pairs generation failed:', error);
    throw error;
  }
}

/**
 * Save GA pairs for a file
 * @param {string} projectId - Project ID
 * @param {string} fileId - File ID
 * @param {Array} gaPairs - Generated GA pairs
 * @param {boolean} appendMode - Whether to append to existing GA pairs
 * @param {Array} existingPairs - Existing GA pairs (for append mode)
 * @returns {Promise<Array>} - Saved GA pairs
 */
async function saveGaPairsForFile(projectId, fileId, gaPairs, appendMode = false, existingPairs = []) {
  try {
    if (appendMode && existingPairs.length > 0) {
      // 追加模式：使用与单文件生成相同的逻辑
      const { createGaPairs } = await import('@/lib/db/ga-pairs');

      const startPairNumber = existingPairs.length + 1;
      const newGaPairData = gaPairs.map((pair, index) => ({
        projectId,
        fileId,
        pairNumber: startPairNumber + index,
        genreTitle: pair.genre?.title || pair.genreTitle || '',
        genreDesc: pair.genre?.description || pair.genreDesc || '',
        audienceTitle: pair.audience?.title || pair.audienceTitle || '',
        audienceDesc: pair.audience?.description || pair.audienceDesc || '',
        isActive: true
      }));

      // 只创建新的GA对，不删除现有的
      await createGaPairs(newGaPairData);

      // 返回所有GA对（现有的+新增的）
      const allPairs = await getGaPairsByFileId(fileId);
      return allPairs;
    } else {
      // Use the database function to save GA pairs
      const result = await saveGaPairs(projectId, fileId, gaPairs);

      // Get the saved pairs to return
      const savedPairs = await getGaPairsByFileId(fileId);
      return savedPairs;
    }
  } catch (error) {
    logger.error('Failed to save GA pairs:', error);
    throw error;
  }
}

/**
 * 任务恢复服务
 * 用于在服务启动时检查并恢复未完成的任务
 */
import { PrismaClient } from '@prisma/client';
import { processAnswerGenerationTask } from './answer-generation';
import { processQuestionGenerationTask } from './question-generation';

const prisma = new PrismaClient();

// 服务初始化标志，确保只执行一次
let initialized = false;

/**
 * 恢复未完成的任务
 * 在应用启动时自动执行一次
 */
export async function recoverPendingTasks() {
  // 如果已经初始化过，直接返回
  if (process.env.INITED) {
    return;
  }

  process.env.INITED = true;

  try {
    console.log('开始检查未完成任务...');

    // 查找所有处理中的任务
    const pendingTasks = await prisma.task.findMany({
      where: {
        status: 0 // 处理中的任务
      }
    });

    if (pendingTasks.length === 0) {
      console.log('没有需要恢复的任务');
      initialized = true;
      return;
    }

    console.log(`找到 ${pendingTasks.length} 个未完成任务，开始恢复...`);

    // 遍历处理每个任务
    for (const task of pendingTasks) {
      try {
        // 根据任务类型调用对应的处理函数
        switch (task.taskType) {
          case 'question-generation':
            // 异步处理，不等待完成
            processQuestionGenerationTask(task).catch(error => {
              console.error(`恢复问题生成任务 ${task.id} 失败:`, error);
            });
            break;
          case 'answer-generation':
            // 异步处理，不等待完成
            processAnswerGenerationTask(task).catch(error => {
              console.error(`恢复答案生成任务 ${task.id} 失败:`, error);
            });
            break;
          default:
            console.warn(`Other Task: ${task.taskType}`);
            await prisma.task.update({
              where: { id: task.id },
              data: {
                status: 2,
                detail: `${task.taskType} Error`,
                note: `${task.taskType} Error`,
                endTime: new Date()
              }
            });
        }
      } catch (error) {
        console.error(`恢复任务 ${task.id} 失败:`, error);
      }
    }

    console.log('任务恢复服务已启动，未完成任务将在后台继续处理');
    initialized = true;
  } catch (error) {
    console.error('任务恢复服务出错:', error);
    // 即使出错也标记为已初始化，避免反复尝试
    initialized = true;
  }
}

// 在模块加载时自动执行恢复
recoverPendingTasks().catch(error => {
  console.error('执行任务恢复失败:', error);
});

import { NextResponse } from 'next/server';
import { getProjectChunks } from '@/lib/file/text-splitter';
import { getTaskConfig } from '@/lib/db/projects';
import { getChunkById } from '@/lib/db/chunks';
import { generateQuestionsForChunk, generateQuestionsForChunkWithGA } from '@/lib/services/questions';

// 批量生成问题
export async function POST(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
    }

    // 获取请求体
    const { model, chunkIds, language = '中文', enableGaExpansion = false } = await request.json();

    if (!model) {
      return NextResponse.json({ error: 'The model cannot be empty' }, { status: 400 });
    }

    // 如果没有指定文本块ID，则获取所有文本块
    let chunks = [];
    if (!chunkIds || chunkIds.length === 0) {
      const result = await getProjectChunks(projectId);
      chunks = result.chunks || [];
    } else {
      // 获取指定的文本块
      chunks = await Promise.all(
        chunkIds.map(async chunkId => {
          const chunk = await getChunkById(chunkId);
          if (chunk) {
            return {
              id: chunk.id,
              content: chunk.content,
              length: chunk.content.length
            };
          }
          return null;
        })
      );
      chunks = chunks.filter(Boolean); // 过滤掉不存在的文本块
    }
    if (chunks.length === 0) {
      return NextResponse.json({ error: 'No valid text blocks found' }, { status: 404 });
    }

    const results = [];
    const errors = [];

    // 获取项目 task-config 信息
    const taskConfig = await getTaskConfig(projectId);
    const { questionGenerationLength } = taskConfig;
    for (const chunk of chunks) {
      try {
        // 根据文本长度自动计算问题数量
        const questionNumber = Math.floor(chunk.length / questionGenerationLength);

        let result;
        if (enableGaExpansion) {
          // 使用GA增强的问题生成
          result = await generateQuestionsForChunkWithGA(projectId, chunk.id, {
            model,
            language,
            number: questionNumber
          });
        } else {
          // 使用标准问题生成
          result = await generateQuestionsForChunk(projectId, chunk.id, {
            model,
            language,
            number: questionNumber
          });
        }

        // 统一处理返回结果格式
        if (result && result.questions && Array.isArray(result.questions)) {
          // GA增强模式的结果格式
          results.push({
            chunkId: chunk.id,
            success: true,
            questions: result.questions,
            total: result.total,
            gaExpansionUsed: result.gaExpansionUsed,
            gaPairsCount: result.gaPairsCount
          });
        } else if (result && result.labelQuestions && Array.isArray(result.labelQuestions)) {
          // 标准模式的结果格式
          results.push({
            chunkId: chunk.id,
            success: true,
            questions: result.labelQuestions,
            total: result.total,
            gaExpansionUsed: false,
            gaPairsCount: 0
          });
        } else {
          errors.push({
            chunkId: chunk.id,
            error: 'Failed to parse questions'
          });
        }
      } catch (error) {
        console.error(`Failed to generate questions for text block ${chunk.id}:`, String(error));
        errors.push({
          chunkId: chunk.id,
          error: error.message || 'Failed to generate questions'
        });
      }
    }

    // 返回生成结果
    return NextResponse.json({
      results,
      errors,
      totalSuccess: results.length,
      totalErrors: errors.length,
      totalChunks: chunks.length
    });
  } catch (error) {
    console.error('Failed to generate questions:', String(error));
    return NextResponse.json({ error: error.message || 'Failed to generate questions' }, { status: 500 });
  }
}

import { NextResponse } from 'next/server';
import { getGaPairsByFileId, toggleGaPairActive, saveGaPairs, createGaPairs } from '@/lib/db/ga-pairs';
import { getUploadFileInfoById } from '@/lib/db/upload-files';
import { generateGaPairs } from '@/lib/services/ga/ga-generation';
import logger from '@/lib/util/logger';
import { db } from '@/lib/db/index';

/**
 * 生成文件的 GA 对
 */
export async function POST(request, { params }) {
  try {
    const { projectId, fileId } = params;
    const { regenerate = false, appendMode = false, language = '中文' } = await request.json();

    // 验证参数
    if (!projectId || !fileId) {
      return NextResponse.json({ error: 'Project ID and File ID are required' }, { status: 400 });
    }

    logger.info(`Starting GA pairs generation for project: ${projectId}, file: ${fileId}, appendMode: ${appendMode}`);

    // 检查文件是否存在
    const file = await getUploadFileInfoById(fileId);
    if (!file || file.projectId !== projectId) {
      return NextResponse.json({ error: 'File not found or does not belong to the project' }, { status: 404 });
    }

    // 获取现有的GA对
    const existingGaPairs = await getGaPairsByFileId(fileId);

    // 如果是追加模式且已有GA对，或者不是重新生成且已存在GA对
    if (!regenerate && !appendMode && existingGaPairs.length > 0) {
      return NextResponse.json({
        success: true,
        message: 'GA pairs already exist for this file',
        data: existingGaPairs
      });
    }

    // 读取文件内容
    const fileContent = await getFileContent(projectId, file.fileName);
    if (!fileContent) {
      return NextResponse.json({ error: 'Failed to read file content' }, { status: 500 });
    }

    logger.info(`File content loaded successfully, length: ${fileContent.length}`);

    // 检查模型配置
    try {
      const { getActiveModel } = await import('@/lib/services/models');
      const activeModel = await getActiveModel(projectId);

      if (!activeModel) {
        logger.error('No active model configuration found');
        return NextResponse.json(
          { error: 'No active AI model configured. Please configure a model in settings first.' },
          { status: 400 }
        );
      }

      logger.info(`Using active model: ${activeModel.provider} - ${activeModel.model}`);
    } catch (modelError) {
      logger.error('Error checking model configuration:', modelError);
      return NextResponse.json(
        { error: 'Failed to load model configuration. Please check your AI model settings.' },
        { status: 500 }
      );
    }

    // 调用 LLM 生成 GA 对
    logger.info(`Generating GA pairs for file: ${file.fileName}`);
    let generatedGaPairs;

    try {
      generatedGaPairs = await generateGaPairs(fileContent, projectId, language);

      if (!generatedGaPairs || generatedGaPairs.length === 0) {
        logger.warn('No GA pairs generated from LLM');
        return NextResponse.json(
          {
            error:
              'No GA pairs could be generated from the file content. The content might be too short or not suitable for GA pair generation.'
          },
          { status: 400 }
        );
      }

      logger.info(`Successfully generated ${generatedGaPairs.length} GA pairs from LLM`);
    } catch (generationError) {
      logger.error('GA pairs generation failed:', generationError);

      // 现有的错误处理逻辑...
      let errorMessage = 'Failed to generate GA pairs';
      if (generationError.message.includes('No active model')) {
        errorMessage = 'No active AI model available. Please configure and activate a model in settings.';
      } else if (generationError.message.includes('API key')) {
        errorMessage = 'Invalid API key or model configuration. Please check your AI model settings.';
      } else if (generationError.message.includes('rate limit')) {
        errorMessage = 'API rate limit exceeded. Please try again later.';
      } else {
        errorMessage = `AI model error: ${generationError.message}`;
      }

      return NextResponse.json({ error: errorMessage }, { status: 500 });
    }

    // 保存到数据库
    try {
      if (appendMode && existingGaPairs.length > 0) {
        // 追加模式：只保存新生成的GA对，不删除现有的
        logger.info(`Appending ${generatedGaPairs.length} new GA pairs to existing ${existingGaPairs.length} pairs`);

        // 为新GA对设置正确的pairNumber
        const startPairNumber = existingGaPairs.length + 1;
        const newGaPairData = generatedGaPairs.map((pair, index) => ({
          projectId,
          fileId,
          pairNumber: startPairNumber + index,
          genreTitle: pair.genre?.title || pair.genreTitle || '',
          genreDesc: pair.genre?.description || pair.genreDesc || '',
          audienceTitle: pair.audience?.title || pair.audienceTitle || '',
          audienceDesc: pair.audience?.description || pair.audienceDesc || '',
          isActive: true
        }));

        // 只创建新的GA对，不删除现有的
        await createGaPairs(newGaPairData);
        logger.info('New GA pairs appended to database successfully');
      } else {
        // 覆盖模式：删除现有的，保存新的
        await saveGaPairs(projectId, fileId, generatedGaPairs);
        logger.info('GA pairs saved to database successfully');
      }
    } catch (saveError) {
      logger.error('Failed to save GA pairs to database:', saveError);
      return NextResponse.json(
        { error: 'Generated GA pairs successfully but failed to save to database' },
        { status: 500 }
      );
    }

    // 获取保存后的所有GA对
    const allGaPairs = await getGaPairsByFileId(fileId);

    if (appendMode && existingGaPairs.length > 0) {
      // 追加模式：只返回新生成的GA对
      const newGaPairs = allGaPairs.slice(existingGaPairs.length);
      logger.info(`Successfully appended ${newGaPairs.length} GA pairs. Total pairs: ${allGaPairs.length}`);

      return NextResponse.json({
        success: true,
        message: `${newGaPairs.length} new GA pairs appended successfully`,
        data: newGaPairs,
        total: allGaPairs.length
      });
    } else {
      // 覆盖模式：返回所有GA对
      logger.info(`Successfully generated and saved ${allGaPairs.length} GA pairs for file: ${file.fileName}`);

      return NextResponse.json({
        success: true,
        message: 'GA pairs generated successfully',
        data: allGaPairs
      });
    }
  } catch (error) {
    logger.error('Unexpected error in GA pairs generation:', error);
    return NextResponse.json(
      { error: error.message || 'Unexpected error occurred during GA pairs generation' },
      { status: 500 }
    );
  }
}

/**
 * 获取文件的 GA 对
 */
export async function GET(request, { params }) {
  try {
    const { projectId, fileId } = params;

    if (!projectId || !fileId) {
      return NextResponse.json({ error: 'Project ID and File ID are required' }, { status: 400 });
    }

    const gaPairs = await getGaPairsByFileId(fileId);

    return NextResponse.json({
      success: true,
      data: gaPairs
    });
  } catch (error) {
    console.error('Error getting GA pairs:', String(error));
    return NextResponse.json({ error: 'Failed to get GA pairs' }, { status: 500 });
  }
}

/**
 * 更新/替换文件的所有 GA 对
 */
export async function PUT(request, { params }) {
  try {
    const { projectId, fileId } = params;
    const body = await request.json();

    if (!projectId || !fileId) {
      return NextResponse.json({ error: 'Project ID and File ID are required' }, { status: 400 });
    }

    const { updates } = body;

    if (!updates || !Array.isArray(updates)) {
      return NextResponse.json({ error: 'Updates array is required' }, { status: 400 });
    }

    logger.info(`Replacing all GA pairs for file ${fileId} with ${updates.length} pairs`);

    // 使用数据库事务确保原子性操作
    const results = await db.$transaction(async tx => {
      // 1. 先删除所有现有的GA对
      await tx.gaPairs.deleteMany({
        where: { fileId }
      });

      // 2. 然后创建新的GA对
      if (updates.length > 0) {
        const gaPairData = updates.map((pair, index) => ({
          projectId,
          fileId,
          pairNumber: index + 1,
          genreTitle: pair.genreTitle || pair.genre?.title || pair.genre || '',
          genreDesc: pair.genreDesc || pair.genre?.description || '',
          audienceTitle: pair.audienceTitle || pair.audience?.title || pair.audience || '',
          audienceDesc: pair.audienceDesc || pair.audience?.description || '',
          isActive: pair.isActive !== undefined ? pair.isActive : true
        }));

        // 验证数据
        for (const data of gaPairData) {
          if (!data.genreTitle || !data.audienceTitle) {
            throw new Error(`Invalid GA pair data: missing genre or audience title`);
          }
        }

        await tx.gaPairs.createMany({ data: gaPairData });
      }

      // 3. 返回新创建的GA对
      return await tx.gaPairs.findMany({
        where: { fileId },
        orderBy: { pairNumber: 'asc' }
      });
    });

    logger.info(`Successfully replaced GA pairs, new count: ${results.length}`);

    return NextResponse.json({
      success: true,
      data: results
    });
  } catch (error) {
    logger.error('Error updating GA pairs:', error);
    return NextResponse.json({ error: error.message || 'Failed to update GA pairs' }, { status: 500 });
  }
}

/**
 * 切换 GA 对激活状态
 */
export async function PATCH(request, { params }) {
  try {
    const { projectId, fileId } = params;
    const body = await request.json();

    if (!projectId || !fileId) {
      return NextResponse.json({ error: 'Project ID and File ID are required' }, { status: 400 });
    }

    const { gaPairId, isActive } = body;

    if (!gaPairId || typeof isActive !== 'boolean') {
      return NextResponse.json({ error: 'GA pair ID and active status are required' }, { status: 400 });
    }

    const updatedPair = await toggleGaPairActive(gaPairId, isActive);

    return NextResponse.json({
      success: true,
      data: updatedPair
    });
  } catch (error) {
    console.error('Error toggling GA pair active status:', String(error));
    return NextResponse.json({ error: 'Failed to toggle GA pair active status' }, { status: 500 });
  }
}

// Helper function to read file content
async function getFileContent(projectId, fileName) {
  try {
    const { getProjectRoot } = await import('@/lib/db/base');
    const path = await import('path');
    const fs = await import('fs');

    const projectRoot = await getProjectRoot();
    const filePath = path.join(projectRoot, projectId, 'files', fileName.replace('.pdf', '.md'));

    return await fs.promises.readFile(filePath, 'utf8');
  } catch (error) {
    logger.error('Failed to read file content:', error);
    return null;
  }
}

import { getProjectRoot } from '@/lib/db/base';
import path from 'path';

export async function getFilePageCount(projectId, fileList) {
  const projectRoot = await getProjectRoot();
  let totalPages = 0;
  const { getPageNum } = await import('pdf2md-js');
  for (const file of fileList) {
    if (file.fileName.endsWith('.pdf')) {
      const filePath = path.join(projectRoot, projectId, 'files', file.fileName);
      try {
        const pageCount = await getPageNum(filePath);
        totalPages += pageCount;
        file.pageCount = pageCount;
      } catch (error) {
        console.error(`Failed to get page count for ${file.fileName}:`, error);
      }
    } else {
      totalPages += 1;
      file.pageCount = 1;
    }
  }
  console.log(`Total pages to process: ${totalPages}`);
  return totalPages;
}

import LLMClient from '@/lib/llm/core/index';
import getQuestionPrompt from '@/lib/llm/prompts/question';
import getQuestionEnPrompt from '@/lib/llm/prompts/questionEn';
import getAddLabelPrompt from '@/lib/llm/prompts/addLabel';
import getAddLabelEnPrompt from '@/lib/llm/prompts/addLabelEn';
import { extractJsonFromLLMOutput } from '@/lib/llm/common/util';
import { getTaskConfig, getProject } from '@/lib/db/projects';
import { getTags } from '@/lib/db/tags';
import { getChunkById } from '@/lib/db/chunks';
import { saveQuestions, saveQuestionsWithGaPair } from '@/lib/db/questions';
import { getActiveGaPairsByFileId } from '@/lib/db/ga-pairs';
import logger from '@/lib/util/logger';

/**
 * 随机移除问题中的问号
 * @param {Array} questions 问题列表
 * @param {Number} probability 移除概率(0-100)
 * @returns {Array} 处理后的问题列表
 */
function randomRemoveQuestionMark(questions, questionMaskRemovingProbability) {
  for (let i = 0; i < questions.length; i++) {
    // 去除问题结尾的空格
    let question = questions[i].trimEnd();

    if (Math.random() * 100 < questionMaskRemovingProbability && (question.endsWith('?') || question.endsWith('？'))) {
      question = question.slice(0, -1);
    }
    questions[i] = question;
  }
  return questions;
}

/**
 * 为指定文本块生成问题
 * @param {String} projectId 项目ID
 * @param {String} chunkId 文本块ID
 * @param {Object} options 选项
 * @param {String} options.model 模型名称
 * @param {String} options.language 语言(中文/en)
 * @param {Number} options.number 问题数量(可选)
 * @returns {Promise<Object>} 生成结果
 */
export async function generateQuestionsForChunk(projectId, chunkId, options) {
  try {
    const { model, language = '中文', number } = options;

    if (!model) {
      throw new Error('模型名称不能为空');
    }

    // 并行获取文本块内容和项目配置
    const [chunk, taskConfig, project] = await Promise.all([
      getChunkById(chunkId),
      getTaskConfig(projectId),
      getProject(projectId)
    ]);

    if (!chunk) {
      throw new Error('文本块不存在');
    }

    // 获取项目配置信息
    const { questionGenerationLength, questionMaskRemovingProbability = 60 } = taskConfig;
    const { globalPrompt, questionPrompt } = project;

    // 创建LLM客户端
    const llmClient = new LLMClient(model);
    // 生成问题的数量，如果未指定，则根据文本长度自动计算
    const questionNumber = number || Math.floor(chunk.content.length / questionGenerationLength);

    // 根据语言选择相应的提示词函数
    const promptFunc = language === 'en' ? getQuestionEnPrompt : getQuestionPrompt;
    const prompt = promptFunc({
      text: chunk.content,
      number: questionNumber,
      language,
      globalPrompt,
      questionPrompt
    });
    const response = await llmClient.getResponse(prompt);

    // 从LLM输出中提取JSON格式的问题列表
    const originalQuestions = extractJsonFromLLMOutput(response);
    const questions = randomRemoveQuestionMark(originalQuestions, questionMaskRemovingProbability);
    if (!questions || !Array.isArray(questions)) {
      throw new Error('生成问题失败');
    }

    // 先获取标签，确保 tags 在后续逻辑中可用
    const tags = await getTags(projectId);
    // 根据语言选择标签提示词函数
    const labelPromptFunc = language === 'en' ? getAddLabelEnPrompt : getAddLabelPrompt;
    const labelPrompt = labelPromptFunc(JSON.stringify(tags), JSON.stringify(questions));

    const labelResponse = await llmClient.getResponse(labelPrompt);
    const labelQuestions = extractJsonFromLLMOutput(labelResponse);

    // 保存问题到数据库
    await saveQuestions(projectId, labelQuestions, chunkId);

    // 返回生成的问题
    return {
      chunkId,
      labelQuestions,
      total: labelQuestions.length
    };
  } catch (error) {
    logger.error('生成问题时出错:', error);
    throw error;
  }
}

function extractLabels(data) {
  if (!Array.isArray(data)) {
    return [];
  }

  return data.map(item => {
    const result = {
      label: item.label
    };

    if (Array.isArray(item.child) && item.child.length > 0) {
      result.child = extractLabels(item.child);
    }

    return result;
  });
}

/**
 * 为指定文本块生成问题（支持GA增强）
 * @param {String} projectId 项目ID
 * @param {String} chunkId 文本块ID
 * @param {Object} options 选项
 * @param {String} options.model 模型名称
 * @param {String} options.language 语言(中文/en)
 * @param {Number} options.number 问题数量(可选)
 * @param {Boolean} options.enableGaExpansion 是否启用GA扩展生成
 * @returns {Promise<Object>} 生成结果
 */
export async function generateQuestionsForChunkWithGA(projectId, chunkId, options) {
  try {
    const { model, language = '中文', number } = options;

    if (!model) {
      throw new Error('模型名称不能为空');
    }

    // 并行获取文本块内容和项目配置
    const [chunk, taskConfig, project] = await Promise.all([
      getChunkById(chunkId),
      getTaskConfig(projectId),
      getProject(projectId)
    ]);

    if (!chunk) {
      throw new Error('文本块不存在');
    }

    // 获取项目配置信息
    const { questionGenerationLength, questionMaskRemovingProbability = 60 } = taskConfig;
    const { globalPrompt, questionPrompt } = project;

    // 检查是否有可用的GA pairs并且启用GA扩展
    let activeGaPairs = [];
    let useGaExpansion = false;

    if (chunk.fileId) {
      try {
        activeGaPairs = await getActiveGaPairsByFileId(chunk.fileId);
        useGaExpansion = activeGaPairs.length > 0;
        logger.info(`检查到 ${activeGaPairs.length} 个激活的GA pairs，${useGaExpansion ? '启用' : '不启用'}GA扩展生成`);
      } catch (error) {
        logger.warn(`获取GA pairs失败，使用标准生成: ${error.message}`);
        useGaExpansion = false;
      }
    }

    // 创建LLM客户端
    const llmClient = new LLMClient(model);

    // 计算基础问题数量
    const baseQuestionNumber = number || Math.floor(chunk.content.length / questionGenerationLength);

    let allGeneratedQuestions = [];
    let totalExpectedQuestions = baseQuestionNumber;

    if (useGaExpansion) {
      // GA扩展模式：为每个GA pair生成基础数量的问题
      totalExpectedQuestions = baseQuestionNumber * activeGaPairs.length;
      logger.info(
        `GA扩展模式：将生成${baseQuestionNumber} 基础问题 × ${activeGaPairs.length} GA pairs = ${totalExpectedQuestions}个总问题`
      );

      // 为每个GA pair生成问题
      for (const gaPair of activeGaPairs) {
        const activeGaPair = {
          genre: `${gaPair.genreTitle}: ${gaPair.genreDesc}`,
          audience: `${gaPair.audienceTitle}: ${gaPair.audienceDesc}`,
          active: gaPair.isActive
        };

        // 根据语言选择相应的提示词函数
        const promptFunc = language === 'en' ? getQuestionEnPrompt : getQuestionPrompt;
        const prompt = promptFunc({
          text: chunk.content,
          number: baseQuestionNumber,
          language,
          globalPrompt,
          questionPrompt,
          activeGaPair: activeGaPair
        });

        const response = await llmClient.getResponse(prompt);
        const originalQuestions = extractJsonFromLLMOutput(response);
        const questions = randomRemoveQuestionMark(originalQuestions, questionMaskRemovingProbability);

        if (!questions || !Array.isArray(questions)) {
          logger.warn(`GA pair ${gaPair.genreTitle}+${gaPair.audienceTitle} 生成问题失败，跳过`);
          continue;
        }

        // 为这批问题添加标签
        const tags = await getTags(projectId);
        const labelPromptFunc = language === 'en' ? getAddLabelEnPrompt : getAddLabelPrompt;
        const labelPrompt = labelPromptFunc(JSON.stringify(tags), JSON.stringify(questions));
        const labelResponse = await llmClient.getResponse(labelPrompt);
        const labelQuestions = extractJsonFromLLMOutput(labelResponse);

        // 保存问题到数据库（关联GA pair）
        await saveQuestionsWithGaPair(projectId, labelQuestions, chunkId, gaPair.id);

        allGeneratedQuestions.push(
          ...labelQuestions.map(q => ({
            ...q,
            gaPairId: gaPair.id,
            gaPairInfo: `${gaPair.genreTitle}+${gaPair.audienceTitle}`
          }))
        );

        logger.info(`GA pair ${gaPair.genreTitle}+${gaPair.audienceTitle} 生成了 ${labelQuestions.length} 个问题`);
      }
    } else {
      // 标准模式：使用原有逻辑
      logger.info(`标准模式：生成 ${baseQuestionNumber} 个问题`);

      const promptFunc = language === 'en' ? getQuestionEnPrompt : getQuestionPrompt;
      const prompt = promptFunc({
        text: chunk.content,
        number: baseQuestionNumber,
        language,
        globalPrompt,
        questionPrompt
      });

      const response = await llmClient.getResponse(prompt);
      const originalQuestions = extractJsonFromLLMOutput(response);
      const questions = randomRemoveQuestionMark(originalQuestions, questionMaskRemovingProbability);

      if (!questions || !Array.isArray(questions)) {
        throw new Error('生成问题失败');
      }

      // 添加标签
      const tags = extractLabels(await getTags(projectId));
      const labelPromptFunc = language === 'en' ? getAddLabelEnPrompt : getAddLabelPrompt;
      const labelPrompt = labelPromptFunc(JSON.stringify(tags), JSON.stringify(questions));
      const labelResponse = await llmClient.getResponse(labelPrompt);
      const labelQuestions = extractJsonFromLLMOutput(labelResponse);

      // 保存问题到数据库（不关联GA pair）
      await saveQuestions(projectId, labelQuestions, chunkId);

      allGeneratedQuestions = labelQuestions;
    }

    // 返回生成的问题
    return {
      chunkId,
      questions: allGeneratedQuestions,
      total: allGeneratedQuestions.length,
      expectedTotal: totalExpectedQuestions,
      gaExpansionUsed: useGaExpansion,
      gaPairsCount: activeGaPairs.length
    };
  } catch (error) {
    logger.error('GA增强问题生成时出错:', error);
    throw error;
  }
}

export default {
  generateQuestionsForChunk,
  generateQuestionsForChunkWithGA
};

'use client';

import { useState } from 'react';
import {
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  Button,
  Box,
  Typography,
  CircularProgress,
  List,
  ListItem,
  ListItemText,
  ListItemSecondaryAction,
  IconButton,
  Alert,
  Paper,
  useTheme,
  Tooltip
} from '@mui/material';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
import DeleteIcon from '@mui/icons-material/Delete';
import { useTranslation } from 'react-i18next';

/**
 * 项目迁移对话框组件
 * @param {Object} props - 组件属性
 * @param {boolean} props.open - 对话框是否打开
 * @param {Function} props.onClose - 关闭对话框的回调函数
 * @param {Array<string>} props.projectIds - 需要迁移的项目ID列表
 */
export default function MigrationDialog({ open, onClose, projectIds = [] }) {
  const { t } = useTranslation();
  const theme = useTheme();
  const [migrating, setMigrating] = useState(false);
  const [success, setSuccess] = useState(false);
  const [error, setError] = useState(null);
  const [migratedCount, setMigratedCount] = useState(0);
  const [taskId, setTaskId] = useState(null);
  const [progress, setProgress] = useState(0);
  const [statusText, setStatusText] = useState('');
  const [processingIds, setProcessingIds] = useState([]);

  // 打开项目目录
  const handleOpenDirectory = async projectId => {
    try {
      setProcessingIds(prev => [...prev, projectId]);

      const response = await fetch('/api/projects/open-directory', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ projectId })
      });

      if (!response.ok) {
        const data = await response.json();
        throw new Error(data.error || t('migration.openDirectoryFailed'));
      }

      // 成功打开目录，不需要特别处理
    } catch (err) {
      console.error('打开目录错误:', err);
      setError(err.message);
    } finally {
      setProcessingIds(prev => prev.filter(id => id !== projectId));
    }
  };

  // 删除项目目录
  const handleDeleteDirectory = async projectId => {
    try {
      if (!window.confirm(t('migration.confirmDelete'))) {
        return;
      }

      setProcessingIds(prev => [...prev, projectId]);

      const response = await fetch('/api/projects/delete-directory', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ projectId })
      });

      if (!response.ok) {
        const data = await response.json();
        throw new Error(data.error || t('migration.deleteDirectoryFailed'));
      }

      // 从列表中移除已删除的项目
      const updatedProjectIds = projectIds.filter(id => id !== projectId);
      // 这里我们不能直接修改 projectIds，因为它是从父组件传入的
      // 但我们可以通知用户界面刷新
      window.location.reload();
    } catch (err) {
      console.error('删除目录错误:', err);
      setError(err.message);
    } finally {
      setProcessingIds(prev => prev.filter(id => id !== projectId));
    }
  };

  // 处理迁移操作
  const handleMigration = async () => {
    try {
      setMigrating(true);
      setError(null);
      setSuccess(false);
      setProgress(0);
      setStatusText(t('migration.starting'));

      // 调用异步迁移接口启动迁移任务
      const response = await fetch('/api/projects/migrate', {
        method: 'POST'
      });

      if (!response.ok) {
        throw new Error(t('migration.failed'));
      }

      const { success, taskId: newTaskId } = await response.json();

      if (!success || !newTaskId) {
        throw new Error(t('migration.startFailed'));
      }

      // 保存任务ID
      setTaskId(newTaskId);
      setStatusText(t('migration.processing'));

      // 开始轮询任务状态
      await pollMigrationStatus(newTaskId);
    } catch (err) {
      console.error('迁移错误:', err);
      setError(err.message);
      setMigrating(false);
    }
  };

  // 轮询迁移任务状态
  const pollMigrationStatus = async id => {
    try {
      // 定义轮询间隔（毫秒）
      const pollInterval = 1000;

      // 发送请求获取任务状态
      const response = await fetch(`/api/projects/migrate?taskId=${id}`);

      if (!response.ok) {
        throw new Error(t('migration.statusFailed'));
      }

      const { success, task } = await response.json();

      if (!success || !task) {
        throw new Error(t('migration.taskNotFound'));
      }

      // 更新进度
      setProgress(task.progress || 0);

      // 根据任务状态更新UI
      if (task.status === 'completed') {
        // 任务完成
        setMigratedCount(task.completed);
        setSuccess(true);
        setMigrating(false);
        setStatusText(t('migration.completed'));

        // 迁移成功后，延迟关闭对话框并刷新页面
        setTimeout(() => {
          onClose();
          window.location.reload();
        }, 2000);
      } else if (task.status === 'failed') {
        // 任务失败
        throw new Error(task.error || t('migration.failed'));
      } else {
        // 任务仍在进行中，继续轮询
        setTimeout(() => pollMigrationStatus(id), pollInterval);

        // 更新状态文本
        if (task.total > 0) {
          setStatusText(
            t('migration.progressStatus', {
              completed: task.completed || 0,
              total: task.total
            })
          );
        }
      }
    } catch (err) {
      console.error('获取迁移状态错误:', err);
      setError(err.message);
      setMigrating(false);
    }
  };

  return (
    <Dialog open={open} onClose={migrating ? undefined : onClose} maxWidth="sm" fullWidth>
      <DialogTitle
        sx={{
          display: 'flex',
          alignItems: 'center',
          gap: 1.5
        }}
      >
        <WarningAmberIcon color="warning" />
        <Typography variant="h6">{t('migration.title')}</Typography>
      </DialogTitle>

      <DialogContent>
        {success ? (
          <Alert severity="success" sx={{ mb: 2 }}>
            {t('migration.success', { count: migratedCount })}
          </Alert>
        ) : error ? (
          <Alert severity="error" sx={{ mb: 2 }}>
            {error}
          </Alert>
        ) : null}

        <Typography variant="body1" sx={{ mb: 2 }}>
          {t('migration.description')}
        </Typography>

        {projectIds.length > 0 && (
          <Box sx={{ mt: 2, mb: 2 }}>
            <Typography variant="subtitle1" sx={{ mb: 1 }}>
              {t('migration.projectsList')}:
            </Typography>
            <Paper variant="outlined" sx={{ maxHeight: 180, overflow: 'auto' }}>
              <List dense>
                {projectIds.map(id => (
                  <ListItem key={id}>
                    <ListItemText primary={id} />
                    <ListItemSecondaryAction>
                      <Tooltip title={t('migration.openDirectory')}>
                        <IconButton
                          edge="end"
                          aria-label="open"
                          onClick={() => handleOpenDirectory(id)}
                          disabled={processingIds.includes(id)}
                          size="small"
                        >
                          <FolderOpenIcon fontSize="small" />
                        </IconButton>
                      </Tooltip>
                      <Tooltip title={t('migration.deleteDirectory')}>
                        <IconButton
                          edge="end"
                          aria-label="delete"
                          onClick={() => handleDeleteDirectory(id)}
                          disabled={processingIds.includes(id)}
                          size="small"
                          sx={{ ml: 1, color: 'error.main' }}
                        >
                          <DeleteIcon fontSize="small" />
                        </IconButton>
                      </Tooltip>
                    </ListItemSecondaryAction>
                  </ListItem>
                ))}
              </List>
            </Paper>
          </Box>
        )}

        {migrating && (
          <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', my: 3, gap: 1.5 }}>
            <CircularProgress variant={progress > 0 ? 'determinate' : 'indeterminate'} value={progress} />
            <Typography variant="body2" color="text.secondary">
              {statusText || t('migration.migrating')}
            </Typography>
            {progress > 0 && (
              <Typography variant="body2" color="text.secondary">
                {progress}%
              </Typography>
            )}
          </Box>
        )}
      </DialogContent>

      <DialogActions>
        <Button onClick={onClose} disabled={migrating}>
          {t('common.cancel')}
        </Button>
        <Button onClick={handleMigration} variant="contained" color="primary" disabled={migrating || success}>
          {migrating ? t('migration.migrating') : t('migration.migrate')}
        </Button>
      </DialogActions>
    </Dialog>
  );
}

'use client';

import {
  Box,
  Typography,
  List,
  ListItem,
  ListItemText,
  IconButton,
  Tooltip,
  Divider,
  CircularProgress,
  Checkbox,
  Button,
  Dialog,
  DialogTitle,
  DialogContent,
  DialogContentText,
  DialogActions,
  FormControlLabel,
  Switch,
  Pagination,
  TextField,
  InputAdornment
} from '@mui/material';
import {
  Visibility as VisibilityIcon,
  Download,
  Delete as DeleteIcon,
  FilePresent as FileIcon,
  Psychology as PsychologyIcon,
  CheckBox as SelectAllIcon,
  CheckBoxOutlineBlank as DeselectAllIcon,
  Search as SearchIcon,
  Clear as ClearIcon
} from '@mui/icons-material';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useAtomValue } from 'jotai';
import { selectedModelInfoAtom } from '@/lib/store';
import MarkdownViewDialog from '../MarkdownViewDialog';
import GaPairsIndicator from '../../mga/GaPairsIndicator';
import i18n from '@/lib/i18n';

export default function FileList({
  theme,
  files = {},
  loading = false,
  onDeleteFile,
  sendToFileUploader,
  projectId,
  setPageLoading,
  currentPage = 1,
  onPageChange,
  isFullscreen = false // 新增参数，用于控制是否处于全屏状态
}) {
  const { t } = useTranslation();

  // 现有的状态
  const [array, setArray] = useState([]);
  const [viewDialogOpen, setViewDialogOpen] = useState(false);
  const [viewContent, setViewContent] = useState('');

  // 新增的批量生成GA对相关状态
  const [batchGenDialogOpen, setBatchGenDialogOpen] = useState(false);
  const [generating, setGenerating] = useState(false);
  const [genError, setGenError] = useState(null);
  const [genResult, setGenResult] = useState(null);
  const [projectModel, setProjectModel] = useState(null);
  const [loadingModel, setLoadingModel] = useState(false);
  const [appendMode, setAppendMode] = useState(false);

  // 搜索相关状态
  const [searchTerm, setSearchTerm] = useState('');
  const [searchLoading, setSearchLoading] = useState(false);

  // 获取当前选中的模型信息
  const selectedModelInfo = useAtomValue(selectedModelInfoAtom);

  // 后端搜索功能
  const handleSearch = async searchValue => {
    if (typeof onPageChange === 'function') {
      setSearchLoading(true);
      try {
        // 调用父组件的页面变更函数，传递搜索参数
        await onPageChange(1, searchValue); // 搜索时重置到第一页
      } catch (error) {
        console.error('搜索失败:', error);
      } finally {
        setSearchLoading(false);
      }
    }
  };

  // 防抖搜索
  useEffect(() => {
    const timer = setTimeout(() => {
      handleSearch(searchTerm);
    }, 500); // 500ms 防抖

    return () => clearTimeout(timer);
  }, [searchTerm]);

  // 清空搜索
  const handleClearSearch = () => {
    setSearchTerm('');
    // 清空搜索时立即触发搜索
    handleSearch('');
  };

  const handleCheckboxChange = (fileId, isChecked) => {
    setArray(prevArray => {
      let newArray;
      const stringFileId = String(fileId);

      if (isChecked) {
        newArray = prevArray.includes(stringFileId) ? prevArray : [...prevArray, stringFileId];
      } else {
        newArray = prevArray.filter(item => item !== stringFileId);
      }

      if (typeof sendToFileUploader === 'function') {
        sendToFileUploader(newArray);
      }
      return newArray;
    });
  };

  // 全选文件（包括所有页面的文件）
  const handleSelectAll = async () => {
    try {
      // 获取项目中所有文件的ID
      const response = await fetch(`/api/projects/${projectId}/files?getAllIds=true`);
      if (!response.ok) {
        throw new Error('获取文件列表失败');
      }

      const data = await response.json();
      const allFileIds = data.allFileIds || [];

      setArray(allFileIds);
      if (typeof sendToFileUploader === 'function') {
        sendToFileUploader(allFileIds);
      }
    } catch (error) {
      console.error('全选文件失败:', error);
      // 如果API调用失败，回退到选择当前页面的文件
      if (files?.data?.length > 0) {
        const currentPageFileIds = files.data.map(file => String(file.id));
        setArray(currentPageFileIds);
        if (typeof sendToFileUploader === 'function') {
          sendToFileUploader(currentPageFileIds);
        }
      }
    }
  };
  // 取消全选
  const handleDeselectAll = () => {
    setArray([]);
    if (typeof sendToFileUploader === 'function') {
      sendToFileUploader([]);
    }
  };

  const handleCloseViewDialog = () => {
    setViewDialogOpen(false);
  };

  // 刷新文本块列表
  const refreshTextChunks = () => {
    if (typeof setPageLoading === 'function') {
      setPageLoading(true);
      setTimeout(() => {
        // 可能需要调用父组件的刷新方法
        sendToFileUploader(array);
        setPageLoading(false);
      }, 500);
    }
  };

  const handleViewContent = async fileId => {
    getFileContent(fileId);
    setViewDialogOpen(true);
  };

  const handleDownload = async (fileId, fileName) => {
    setPageLoading(true);
    const text = await getFileContent(fileId);

    // Modify the filename if it ends with .pdf
    let downloadName = fileName || 'download.txt';
    if (downloadName.toLowerCase().endsWith('.pdf')) {
      downloadName = downloadName.slice(0, -4) + '.md';
    }

    const blob = new Blob([text.content], { type: 'text/plain' });
    const url = URL.createObjectURL(blob);

    const a = document.createElement('a');
    a.href = url;
    a.download = downloadName;

    document.body.appendChild(a);
    a.click();

    document.body.removeChild(a);
    URL.revokeObjectURL(url);

    setPageLoading(false);
  };

  const getFileContent = async fileId => {
    try {
      const response = await fetch(`/api/projects/${projectId}/preview/${fileId}`);
      if (!response.ok) {
        throw new Error(t('textSplit.fetchChunksFailed'));
      }
      const data = await response.json();
      setViewContent(data);
      return data;
    } catch (error) {
      console.error(t('textSplit.fetchChunksError'), error);
    }
  };

  const formatFileSize = size => {
    if (size < 1024) {
      return size + 'B';
    } else if (size < 1024 * 1024) {
      return (size / 1024).toFixed(2) + 'KB';
    } else if (size < 1024 * 1024 * 1024) {
      return (size / 1024 / 1024).toFixed(2) + 'MB';
    } else {
      return (size / 1024 / 1024 / 1024).toFixed(2) + 'GB';
    }
  };

  // 新增：获取项目特定的默认模型信息
  const fetchProjectModel = async () => {
    try {
      setLoadingModel(true);

      // 首先获取项目信息
      const response = await fetch(`/api/projects/${projectId}`);
      if (!response.ok) {
        throw new Error(t('gaPairs.fetchProjectInfoFailed', { status: response.status }));
      }

      const projectData = await response.json();

      // 获取模型配置
      const modelResponse = await fetch(`/api/projects/${projectId}/model-config`);
      if (!modelResponse.ok) {
        throw new Error(t('gaPairs.fetchModelConfigFailed', { status: modelResponse.status }));
      }

      const modelConfigData = await modelResponse.json();

      if (modelConfigData.data && Array.isArray(modelConfigData.data)) {
        // 优先使用项目默认模型
        let targetModel = null;

        if (projectData.defaultModelConfigId) {
          targetModel = modelConfigData.data.find(model => model.id === projectData.defaultModelConfigId);
        }

        // 如果没有默认模型，使用第一个可用的模型
        if (!targetModel) {
          targetModel = modelConfigData.data.find(
            m => m.modelName && m.endpoint && (m.providerId === 'ollama' || m.apiKey)
          );
        }

        if (targetModel) {
          setProjectModel(targetModel);
        }
      }
    } catch (error) {
      console.error(t('gaPairs.fetchProjectModelError'), error);
    } finally {
      setLoadingModel(false);
    }
  };

  // 新增：批量生成GA对的处理函数
  const handleBatchGenerateGAPairs = async () => {
    if (array.length === 0) {
      setGenError(t('gaPairs.selectAtLeastOneFile'));
      return;
    }

    const modelToUse = projectModel || selectedModelInfo;

    if (!modelToUse || !modelToUse.id) {
      setGenError(t('gaPairs.noDefaultModel'));
      return;
    }

    // 检查模型配置是否完整
    if (!modelToUse.modelName || !modelToUse.endpoint) {
      setGenError('模型配置不完整，请检查模型设置');
      return;
    }

    // 检查API密钥（除了ollama模型）
    if (modelToUse.providerId !== 'ollama' && !modelToUse.apiKey) {
      setGenError(t('gaPairs.missingApiKey'));
      return;
    }

    try {
      setGenerating(true);
      setGenError(null);
      setGenResult(null);

      const stringFileIds = array.map(id => String(id));

      // 获取当前语言环境
      const currentLanguage = i18n.language === 'en' ? 'en' : '中文';

      const requestData = {
        fileIds: stringFileIds,
        modelConfigId: modelToUse.id,
        language: currentLanguage,
        appendMode: appendMode
      };

      const response = await fetch(`/api/projects/${projectId}/batch-generateGA`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(requestData)
      });

      const responseText = await response.text();

      if (!response.ok) {
        const errorData = await response
          .json()
          .catch(() => ({ error: t('gaPairs.requestFailed', { status: response.status }) }));
        throw new Error(errorData.error || t('gaPairs.requestFailed', { status: response.status }));
      }

      const result = JSON.parse(responseText);

      if (result.success) {
        setGenResult({
          total: result.data?.length || 0,
          success: result.data?.filter(r => r.success).length || 0
        });

        // 成功后清空选择状态
        setArray([]);
        if (typeof sendToFileUploader === 'function') {
          sendToFileUploader([]);
        }

        console.log(t('gaPairs.batchGenerationSuccess', { count: result.summary?.success || 0 }));

        //发送全局刷新事件
        const successfulFileIds = result.data?.filter(item => item.success)?.map(item => String(item.fileId)) || [];

        if (successfulFileIds.length > 0) {
          window.dispatchEvent(
            new CustomEvent('refreshGaPairsIndicators', {
              detail: {
                projectId,
                fileIds: successfulFileIds
              }
            })
          );
        }
      } else {
        setGenError(result.error || t('gaPairs.generationFailed'));
      }
    } catch (error) {
      console.error(t('gaPairs.batchGenerationFailed'), error);
      setGenError(t('gaPairs.generationError', { error: error.message || t('common.unknownError') }));
    } finally {
      setGenerating(false);
    }
  };

  // 新增：打开批量生成对话框
  const openBatchGenDialog = () => {
    // 如果没有选中文件，自动选中所有文件
    if (array.length === 0 && files?.data?.length > 0) {
      const allFileIds = files.data.map(file => String(file.id));
      setArray(allFileIds);
      if (typeof sendToFileUploader === 'function') {
        sendToFileUploader(allFileIds);
      }
    }

    // 获取项目模型配置
    fetchProjectModel();
    setBatchGenDialogOpen(true);
  };

  // 新增：关闭批量生成对话框
  const closeBatchGenDialog = () => {
    setBatchGenDialogOpen(false);
    setGenError(null);
    setGenResult(null);
    setAppendMode(false); // 重置追加模式
  };

  return (
    <Box
      sx={{
        height: '100%',
        p: 3,
        border: `1px solid ${theme.palette.divider}`,
        borderRadius: 2,
        bgcolor: theme.palette.background.paper,
        width: '100%',
        maxWidth: '100%',
        overflow: 'hidden'
      }}
    >
      {/* 标题和按钮区域 */}
      <Box sx={{ mb: 2 }}>
        {/* 第一行：标题和按钮 */}
        <Box
          sx={{
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            mb: isFullscreen && files.total > 0 ? 2 : 0
          }}
        >
          <Typography variant="subtitle1">{t('textSplit.uploadedDocuments', { count: files.total })}</Typography>

          {/* 批量生成GA对按钮 */}
          {files.total > 0 && (
            <Box sx={{ display: 'flex', gap: 1 }}>
              {/* 全选/取消全选按钮 */}
              {array.length === files.total ? (
                <Button
                  variant="outlined"
                  size="small"
                  startIcon={<SelectAllIcon />}
                  onClick={handleDeselectAll}
                  disabled={loading}
                >
                  {t('gaPairs.deselectAllFiles')}
                </Button>
              ) : (
                <Button
                  variant="outlined"
                  size="small"
                  startIcon={<DeselectAllIcon />}
                  onClick={handleSelectAll}
                  disabled={loading}
                >
                  {t('gaPairs.selectAllFiles')}
                </Button>
              )}

              {/* 批量生成GA对按钮 */}
              <Button
                variant="contained"
                color="primary"
                size="small"
                startIcon={<PsychologyIcon />}
                onClick={openBatchGenDialog}
                disabled={loading}
              >
                {t('gaPairs.batchGenerate')}
              </Button>
            </Box>
          )}
        </Box>

        {/* 第二行：搜索框 - 在全屏展示时显示，或者有搜索内容时显示 */}
        {isFullscreen && (files.total > 0 || searchTerm) && (
          <Box>
            <TextField
              size="small"
              placeholder={t('textSplit.searchFiles', { defaultValue: '搜索文件名...' })}
              value={searchTerm}
              onChange={e => setSearchTerm(e.target.value)}
              InputProps={{
                startAdornment: (
                  <InputAdornment position="start">
                    <SearchIcon color="action" />
                  </InputAdornment>
                ),
                endAdornment: searchTerm && (
                  <InputAdornment position="end">
                    <IconButton size="small" onClick={handleClearSearch} edge="end">
                      <ClearIcon />
                    </IconButton>
                  </InputAdornment>
                )
              }}
              sx={{ width: '100%', maxWidth: 400 }}
            />
            {(searchTerm || searchLoading) && (
              <Typography variant="body2" color="textSecondary" sx={{ mt: 1, textAlign: 'center' }}>
                {searchLoading
                  ? '搜索中...'
                  : searchTerm
                    ? t('textSplit.searchResults', {
                        count: files?.data?.length || 0,
                        total: files.total,
                        defaultValue: `找到 ${files?.data?.length || 0} 个文件（共 ${files.total} 个）`
                      })
                    : null}
              </Typography>
            )}
          </Box>
        )}
      </Box>

      {loading ? (
        <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
          <CircularProgress size={24} />
        </Box>
      ) : files.total === 0 ? (
        <Box sx={{ p: 4, textAlign: 'center' }}>
          <Typography variant="body2" color="textSecondary">
            {searchTerm
              ? // 搜索无结果
                t('textSplit.noSearchResults', {
                  searchTerm,
                  defaultValue: `未找到包含 "${searchTerm}" 的文件`
                })
              : // 真的没有上传文件
                t('textSplit.noFilesUploaded', {
                  defaultValue: '暂未上传文件'
                })}
          </Typography>
        </Box>
      ) : !files?.data || files.data.length === 0 ? (
        <Box sx={{ p: 4, textAlign: 'center' }}>
          <Typography variant="body2" color="textSecondary">
            {searchTerm
              ? // 搜索有结果但当前页没数据
                t('textSplit.noResultsOnCurrentPage', {
                  defaultValue: '当前页面没有搜索结果，请返回第一页查看'
                })
              : // 当前页没数据但总数不为0
                t('textSplit.noDataOnCurrentPage', {
                  defaultValue: '当前页面没有数据'
                })}
          </Typography>
        </Box>
      ) : (
        <>
          <List
            sx={{
              maxHeight: isFullscreen ? 'none' : '200px', // 根据 isFullscreen 控制最大高度
              overflow: 'auto',
              width: '100%'
            }}
            dense // 使列表项更紧凑，减少高度
          >
            {files?.data?.map((file, index) => (
              <Box key={index}>
                <ListItem
                  sx={{
                    display: 'flex',
                    justifyContent: 'space-between',
                    flexWrap: 'nowrap',
                    pr: 0 // 移除右侧内边距，便于自定义操作区域位置
                  }}
                >
                  {/* 文件信息区域 */}
                  <Box
                    sx={{
                      display: 'flex',
                      alignItems: 'center',
                      overflow: 'hidden', // 隐藏溢出内容
                      maxWidth: '70%', // 限制文件信息区域最大宽度
                      flexGrow: 1,
                      mr: 2 // 与操作区域保持间距
                    }}
                  >
                    <FileIcon color="primary" sx={{ mr: 1, flexShrink: 0 }} />
                    <Tooltip title={`${file.fileName}（${t('textSplit.viewDetails')}）`}>
                      <ListItemText
                        style={{ cursor: 'pointer', overflow: 'hidden' }}
                        onClick={() => handleViewContent(file.id)}
                        primary={
                          <Typography
                            noWrap // 文本不换行并显示省略号
                            variant="body1"
                            component="div"
                          >
                            {file.fileName}
                          </Typography>
                        }
                        secondary={
                          <Typography
                            noWrap // 文本不换行并显示省略号
                            variant="body2"
                            color="textSecondary"
                            component="div"
                          >
                            {`${formatFileSize(file.size)} · ${new Date(file.createAt).toLocaleString()}`}
                          </Typography>
                        }
                      />
                    </Tooltip>
                  </Box>

                  {/* 操作按钮区域 */}
                  <Box
                    sx={{
                      display: 'flex',
                      flexShrink: 0, // 防止操作区域被压缩
                      alignItems: 'center'
                    }}
                  >
                    <Checkbox
                      sx={{ ml: 1 }}
                      checked={array.includes(String(file.id))}
                      onChange={e => handleCheckboxChange(file.id, e.target.checked)}
                    />
                    <GaPairsIndicator projectId={projectId} fileId={file.id} fileName={file.fileName} />
                    <Tooltip title={t('textSplit.download')}>
                      <IconButton color="primary" onClick={() => handleDownload(file.id, file.fileName)}>
                        <Download />
                      </IconButton>
                    </Tooltip>
                    <Tooltip title={t('textSplit.deleteFile')}>
                      <IconButton color="error" onClick={() => onDeleteFile(file.id, file.fileName)}>
                        <DeleteIcon />
                      </IconButton>
                    </Tooltip>
                  </Box>
                </ListItem>
                {index < files.data.length - 1 && <Divider />}
              </Box>
            ))}
          </List>

          {/* 分页控件 */}
          {files.total > 10 && (
            <Box sx={{ display: 'flex', justifyContent: 'center', mt: 2 }}>
              <Pagination
                count={Math.ceil(files.total / 10)} // 假设每页10个文件
                page={currentPage}
                onChange={(event, page) => onPageChange && onPageChange(page)}
                color="primary"
                showFirstButton
                showLastButton
              />
            </Box>
          )}
        </>
      )}

      {/* 现有的文本块详情对话框 */}
      <MarkdownViewDialog
        open={viewDialogOpen}
        text={viewContent}
        onClose={handleCloseViewDialog}
        projectId={projectId}
        onSaveSuccess={refreshTextChunks}
      />

      {/* 新增：批量生成GA对对话框 */}
      <Dialog open={batchGenDialogOpen} onClose={closeBatchGenDialog} maxWidth="md" fullWidth>
        <DialogTitle>批量生成GA对</DialogTitle>
        <DialogContent>
          {!genResult && (
            <DialogContentText>
              {t('gaPairs.batchGenerateDescription', { count: array.length })}

              {/* 追加模式选择 */}
              <Box sx={{ mt: 2, mb: 2 }}>
                <FormControlLabel
                  control={
                    <Switch checked={appendMode} onChange={e => setAppendMode(e.target.checked)} color="primary" />
                  }
                  label={`${t('gaPairs.appendMode')}（${t('gaPairs.appendModeDescription')}）`}
                />
              </Box>

              {loadingModel ? (
                <Box sx={{ mt: 1, display: 'flex', alignItems: 'center' }}>
                  <CircularProgress size={16} sx={{ mr: 1 }} />
                  <Typography variant="body2">{t('gaPairs.loadingProjectModel')}</Typography>
                </Box>
              ) : projectModel ? (
                <Box sx={{ mt: 1 }}>
                  <Typography variant="body2" color="textSecondary">
                    {t('gaPairs.usingModel')}:{' '}
                    <strong>
                      {projectModel.providerName}: {projectModel.modelName}
                    </strong>
                  </Typography>
                </Box>
              ) : (
                <Box sx={{ mt: 1 }}>
                  <Typography variant="body2" color="error">
                    {t('gaPairs.noDefaultModel')}
                  </Typography>
                </Box>
              )}
            </DialogContentText>
          )}

          {genError && (
            <Box sx={{ mt: 2, p: 2, bgcolor: 'error.light', borderRadius: 1 }}>
              <Typography variant="body2" color="error.contrastText">
                {genError}
              </Typography>
            </Box>
          )}

          {genResult && (
            <Box sx={{ mt: 2, p: 2, bgcolor: 'success.light', borderRadius: 1 }}>
              <Typography variant="body2" color="success.contrastText">
                {t('gaPairs.batchGenCompleted', { success: genResult.success, total: genResult.total })}
              </Typography>
            </Box>
          )}
        </DialogContent>
        <DialogActions>
          <Button onClick={closeBatchGenDialog}>{genResult ? t('common.close') : t('common.cancel')}</Button>
          {!genResult && (
            <Button
              onClick={handleBatchGenerateGAPairs}
              variant="contained"
              disabled={generating || array.length === 0 || !projectModel}
              startIcon={generating ? <CircularProgress size={20} /> : <PsychologyIcon />}
            >
              {generating ? t('gaPairs.generating') : t('gaPairs.startGeneration')}
            </Button>
          )}
        </DialogActions>
      </Dialog>
    </Box>
  );
}

'use client';

import React, { useState, useEffect } from 'react';
import { Badge, IconButton, Tooltip, Box, CircularProgress } from '@mui/material';
import TaskAltIcon from '@mui/icons-material/TaskAlt';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/navigation';
import useFileProcessingStatus from '@/hooks/useFileProcessingStatus';
import axios from 'axios';

// 任务图标组件
export default function TaskIcon({ projectId, theme }) {
  const { t } = useTranslation();
  const router = useRouter();
  const [tasks, setTasks] = useState([]);
  const [polling, setPolling] = useState(false);
  const { setTaskFileProcessing, setTask } = useFileProcessingStatus();

  // 获取项目的未完成任务列表
  const fetchPendingTasks = async () => {
    if (!projectId) return;

    try {
      const response = await axios.get(`/api/projects/${projectId}/tasks/list?status=0`);
      if (response.data?.code === 0) {
        const tasks = response.data.data || [];
        setTasks(tasks);
        // 检查是否有文件处理任务正在进行
        const hasActiveFileTask = tasks.some(
          task => task.projectId === projectId && task.taskType === 'file-processing'
        );
        setTaskFileProcessing(hasActiveFileTask);
        //存在文件处理任务，将任务信息传递给共享状态
        if (hasActiveFileTask) {
          const activeTask = tasks.find(task => task.projectId === projectId && task.taskType === 'file-processing');
          // 解析任务详情信息
          const detailInfo = JSON.parse(activeTask.detail);
          setTask(detailInfo);
        }
      }
    } catch (error) {
      console.error('获取任务列表失败:', error);
    }
  };

  // 初始化时获取任务列表
  useEffect(() => {
    if (projectId) {
      fetchPendingTasks();

      // 启动轮询
      const intervalId = setInterval(() => {
        fetchPendingTasks();
      }, 10000); // 每10秒轮询一次

      setPolling(true);

      return () => {
        clearInterval(intervalId);
        setPolling(false);
      };
    }
  }, [projectId]);

  // 打开任务列表页面
  const handleOpenTaskList = () => {
    router.push(`/projects/${projectId}/tasks`);
  };

  // 图标渲染逻辑
  const renderTaskIcon = () => {
    const pendingTasks = tasks.filter(task => task.status === 0);

    if (pendingTasks.length > 0) {
      // 当有任务处理中时，显示 loading 状态同时保留徽标
      return (
        <Badge badgeContent={pendingTasks.length} color="error">
          <CircularProgress size={20} color="inherit" />
        </Badge>
      );
    }

    // 没有处理中的任务时，显示完成图标
    return <TaskAltIcon fontSize="small" />;
  };

  // 悬停提示文本
  const getTooltipText = () => {
    const pendingTasks = tasks.filter(task => task.status === 0);

    if (pendingTasks.length > 0) {
      return t('tasks.pending', { count: pendingTasks.length });
    }

    return t('tasks.completed');
  };

  if (!projectId) return null;

  return (
    <Tooltip title={getTooltipText()}>
      <IconButton
        onClick={handleOpenTaskList}
        size="small"
        sx={{
          bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(255, 255, 255, 0.15)',
          color: theme.palette.mode === 'dark' ? 'inherit' : 'white',
          p: 1,
          borderRadius: 1.5,
          '&:hover': {
            bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.25)'
          },
          ml: 2
        }}
      >
        {renderTaskIcon()}
      </IconButton>
    </Tooltip>
  );
}

'use client';

import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
  Container,
  Typography,
  Box,
  Button,
  Paper,
  Tabs,
  Tab,
  CircularProgress,
  Divider,
  Checkbox,
  TextField,
  InputAdornment,
  Stack,
  Dialog,
  DialogActions,
  DialogContent,
  DialogContentText,
  DialogTitle,
  LinearProgress,
  Select,
  MenuItem,
  useTheme,
  Tooltip
} from '@mui/material';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import DeleteIcon from '@mui/icons-material/Delete';
import i18n from '@/lib/i18n';
import SearchIcon from '@mui/icons-material/Search';
import AddIcon from '@mui/icons-material/Add';
import QuestionListView from '@/components/questions/QuestionListView';
import QuestionTreeView from '@/components/questions/QuestionTreeView';
import TabPanel from '@/components/text-split/components/TabPanel';
import useTaskSettings from '@/hooks/useTaskSettings';
import QuestionEditDialog from './components/QuestionEditDialog';
import { useQuestionEdit } from './hooks/useQuestionEdit';
import axios from 'axios';
import { toast } from 'sonner';
import { useDebounce } from '@/hooks/useDebounce';
import { useAtomValue } from 'jotai/index';
import { selectedModelInfoAtom } from '@/lib/store';
import { useGenerateDataset } from '@/hooks/useGenerateDataset';
import request from '@/lib/util/request';

export default function QuestionsPage({ params }) {
  const { t } = useTranslation();
  const theme = useTheme();
  const { projectId } = params;
  const [loading, setLoading] = useState(true);
  const [questions, setQuestions] = useState({});
  const [currentPage, setCurrentPage] = useState(1);
  const [pageSize, setPageSize] = useState(10);
  const [answerFilter, setAnswerFilter] = useState('all'); // 'all', 'answered', 'unanswered'
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm);
  const [tags, setTags] = useState([]);
  const model = useAtomValue(selectedModelInfoAtom);
  const { generateMultipleDataset } = useGenerateDataset();
  const [activeTab, setActiveTab] = useState(0);
  const [selectedQuestions, setSelectedQuestions] = useState([]);

  const getQuestionList = async () => {
    try {
      // 获取问题列表
      const questionsResponse = await axios.get(
        `/api/projects/${projectId}/questions?page=${currentPage}&size=10&status=${answerFilter}&input=${searchTerm}`
      );
      if (questionsResponse.status !== 200) {
        throw new Error(t('common.fetchError'));
      }
      setQuestions(questionsResponse.data || {});

      // 获取标签树
      const tagsResponse = await axios.get(`/api/projects/${projectId}/tags`);
      if (tagsResponse.status !== 200) {
        throw new Error(t('common.fetchError'));
      }
      setTags(tagsResponse.data.tags || []);

      setLoading(false);
    } catch (error) {
      console.error(t('common.fetchError'), error);
      toast.error(error.message);
    }
  };

  useEffect(() => {
    getQuestionList();
  }, [currentPage, answerFilter, debouncedSearchTerm]);

  const [processing, setProcessing] = useState(false);

  const { taskSettings } = useTaskSettings(projectId);

  // 进度状态
  const [progress, setProgress] = useState({
    total: 0, // 总共选择的问题数量
    completed: 0, // 已处理完成的数量
    percentage: 0, // 进度百分比
    datasetCount: 0 // 已生成的数据集数量
  });

  const {
    editDialogOpen,
    editMode,
    editingQuestion,
    handleOpenCreateDialog,
    handleOpenEditDialog,
    handleCloseDialog,
    handleSubmitQuestion
  } = useQuestionEdit(projectId, updatedQuestion => {
    getQuestionList();
    toast.success(t('questions.operationSuccess'));
  });

  const [confirmDialog, setConfirmDialog] = useState({
    open: false,
    title: '',
    content: '',
    confirmAction: null
  });

  // 获取所有数据
  useEffect(() => {
    getQuestionList();
  }, [projectId]);

  // 处理标签页切换
  const handleTabChange = (event, newValue) => {
    setActiveTab(newValue);
  };

  // 处理问题选择
  const handleSelectQuestion = (questionKey, newSelected) => {
    if (newSelected) {
      // 处理批量选择的情况
      setSelectedQuestions(newSelected);
    } else {
      // 处理单个问题选择的情况
      setSelectedQuestions(prev => {
        if (prev.includes(questionKey)) {
          return prev.filter(id => id !== questionKey);
        } else {
          return [...prev, questionKey];
        }
      });
    }
  };

  // 全选/取消全选
  const handleSelectAll = async () => {
    if (selectedQuestions.length > 0) {
      setSelectedQuestions([]);
    } else {
      const response = await axios.get(
        `/api/projects/${projectId}/questions?status=${answerFilter}&input=${searchTerm}&selectedAll=1`
      );
      setSelectedQuestions(response.data.map(dataset => dataset.id));
    }
  };

  const handleBatchGenerateAnswers_backup = async () => {
    if (selectedQuestions.length === 0) {
      toast.warning(t('questions.noQuestionsSelected'));
      return;
    }
    let data = questions.data.filter(question => selectedQuestions.includes(question.id));
    await generateMultipleDataset(projectId, data);
    await getQuestionList();
  };

  // 并行处理数组的辅助函数，限制并发数
  const processInParallel = async (items, processFunction, concurrencyLimit) => {
    const results = [];
    const inProgress = new Set();
    const queue = [...items];

    while (queue.length > 0 || inProgress.size > 0) {
      // 如果有空闲槽位且队列中还有任务，启动新任务
      while (inProgress.size < concurrencyLimit && queue.length > 0) {
        const item = queue.shift();
        const promise = processFunction(item).then(result => {
          inProgress.delete(promise);
          return result;
        });
        inProgress.add(promise);
        results.push(promise);
      }

      // 等待其中一个任务完成
      if (inProgress.size > 0) {
        await Promise.race(inProgress);
      }
    }

    return Promise.all(results);
  };

  const handleBatchGenerateAnswers = async () => {
    if (selectedQuestions.length === 0) {
      toast.warning(t('questions.noQuestionsSelected'));
      return;
    }

    if (!model) {
      toast.warning(t('models.configNotFound'));
      return;
    }

    try {
      setProgress({
        total: selectedQuestions.length,
        completed: 0,
        percentage: 0,
        datasetCount: 0
      });

      // 然后设置处理状态为真，确保进度条显示
      setProcessing(true);

      toast.info(t('questions.batchGenerateStart', { count: selectedQuestions.length }));

      // 单个问题处理函数
      const processQuestion = async questionId => {
        try {
          console.log('开始生成数据集:', { questionId });
          const language = i18n.language === 'zh-CN' ? '中文' : 'en';
          // 调用API生成数据集
          const response = await request(`/api/projects/${projectId}/datasets`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              questionId,
              model,
              language
            })
          });

          if (!response.ok) {
            const errorData = await response.json();
            console.error(t('datasets.generateError'), errorData.error || t('datasets.generateFailed'));

            // 更新进度状态（即使失败也计入已处理）
            setProgress(prev => {
              const completed = prev.completed + 1;
              const percentage = Math.round((completed / prev.total) * 100);

              return {
                ...prev,
                completed,
                percentage
              };
            });

            return { success: false, questionId, error: errorData.error || t('datasets.generateFailed') };
          }

          const data = await response.json();

          // 更新进度状态
          setProgress(prev => {
            const completed = prev.completed + 1;
            const percentage = Math.round((completed / prev.total) * 100);
            const datasetCount = prev.datasetCount + 1;

            return {
              ...prev,
              completed,
              percentage,
              datasetCount
            };
          });

          console.log(`数据集生成成功: ${questionId}`);
          return { success: true, questionId, data: data.dataset };
        } catch (error) {
          console.error('生成数据集失败:', error);

          // 更新进度状态（即使失败也计入已处理）
          setProgress(prev => {
            const completed = prev.completed + 1;
            const percentage = Math.round((completed / prev.total) * 100);

            return {
              ...prev,
              completed,
              percentage
            };
          });

          return { success: false, questionId, error: error.message };
        }
      };

      // 并行处理所有问题，最多同时处理2个
      const results = await processInParallel(selectedQuestions, processQuestion, taskSettings.concurrencyLimit);

      // 刷新数据
      getQuestionList();

      // 处理完成后设置结果消息
      const successCount = results.filter(r => r.success).length;
      const failCount = results.filter(r => !r.success).length;

      if (failCount > 0) {
        toast.warning(
          t('datasets.partialSuccess', {
            successCount,
            total: selectedQuestions.length,
            failCount
          })
        );
      } else {
        toast.success(t('common.success', { successCount }));
      }
    } catch (error) {
      console.error('生成数据集出错:', error);
      toast.error(error.message || '生成数据集失败');
    } finally {
      // 延迟关闭处理状态，确保用户可以看到完成的进度
      setTimeout(() => {
        setProcessing(false);
        // 再次延迟重置进度状态
        setTimeout(() => {
          setProgress({
            total: 0,
            completed: 0,
            percentage: 0,
            datasetCount: 0
          });
        }, 500);
      }, 2000); // 延迟关闭处理状态，让用户看到完成的进度
    }
  };

  // 处理删除问题
  const confirmDeleteQuestion = questionId => {
    // 显示确认对话框
    setConfirmDialog({
      open: true,
      title: t('common.confirmDelete'),
      content: t('common.confirmDeleteQuestion'),
      confirmAction: () => executeDeleteQuestion(questionId)
    });
  };

  // 执行删除问题的操作
  const executeDeleteQuestion = async questionId => {
    toast.promise(axios.delete(`/api/projects/${projectId}/questions/${questionId}`), {
      loading: '数据删除中',
      success: data => {
        setSelectedQuestions(prev =>
          prev.includes(questionId) ? prev.filter(id => id !== questionId) : [...prev, questionId]
        );
        getQuestionList();
        return t('common.deleteSuccess');
      },
      error: error => {
        return error.response?.data?.message || '删除失败';
      }
    });
  };

  // 处理删除问题的入口函数
  const handleDeleteQuestion = questionId => {
    confirmDeleteQuestion(questionId);
  };

  // 处理自动生成数据集
  const handleAutoGenerateDatasets = async () => {
    try {
      if (!model) {
        toast.error(t('questions.selectModelFirst', { defaultValue: '请先选择模型' }));
        return;
      }

      // 调用创建任务接口
      const response = await axios.post(`/api/projects/${projectId}/tasks`, {
        taskType: 'answer-generation',
        modelInfo: model,
        language: i18n.language
      });

      if (response.data?.code === 0) {
        toast.success(t('tasks.createSuccess', { defaultValue: '后台任务已创建，系统将自动处理未生成答案的问题' }));
      } else {
        toast.error(t('tasks.createFailed', { defaultValue: '创建后台任务失败' }));
      }
    } catch (error) {
      console.error('创建任务失败:', error);
      toast.error(t('tasks.createFailed', { defaultValue: '创建任务失败' }) + ': ' + error.message);
    }
  };

  // 确认批量删除问题
  const confirmBatchDeleteQuestions = () => {
    if (selectedQuestions.length === 0) {
      toast.warning('请先选择问题');
      return;
    }
    // 显示确认对话框
    setConfirmDialog({
      open: true,
      title: '确认批量删除问题',
      content: `您确定要删除选中的 ${selectedQuestions.length} 个问题吗？此操作不可恢复。`,
      confirmAction: executeBatchDeleteQuestions
    });
  };

  // 执行批量删除问题
  const executeBatchDeleteQuestions = async () => {
    toast.promise(
      axios.delete(`/api/projects/${projectId}/questions/batch-delete`, { data: { questionIds: selectedQuestions } }),
      {
        loading: `正在删除 ${selectedQuestions.length} 个问题...`,
        success: data => {
          getQuestionList();
          setSelectedQuestions([]);
          return `成功删除 ${selectedQuestions.length} 个问题`;
        },
        error: error => {
          return error.response?.data?.message || '批量删除问题失败';
        }
      }
    );
  };

  // 处理批量删除问题的入口函数
  const handleBatchDeleteQuestions = () => {
    confirmBatchDeleteQuestions();
  };

  if (loading) {
    return (
      <Container maxWidth="lg" sx={{ mt: 4 }}>
        <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '70vh' }}>
          <CircularProgress />
        </Box>
      </Container>
    );
  }

  return (
    <Container maxWidth="lg" sx={{ mt: 4, mb: 8 }}>
      {/* 处理中的进度显示 - 全局蒙版样式 */}
      {processing && (
        <Box
          sx={{
            position: 'fixed',
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            width: '100vw',
            height: '100vh',
            backgroundColor: 'rgba(0, 0, 0, 0.7)',
            zIndex: 9999,
            display: 'flex',
            flexDirection: 'column',
            justifyContent: 'center',
            alignItems: 'center'
          }}
        >
          <Paper
            elevation={6}
            sx={{
              width: '90%',
              maxWidth: 500,
              p: 3,
              borderRadius: 2,
              textAlign: 'center'
            }}
          >
            <Typography variant="h6" sx={{ mb: 2, color: 'primary.main', fontWeight: 'bold' }}>
              {t('datasets.generatingDataset')}
            </Typography>

            <Box sx={{ mb: 3 }}>
              <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
                <Typography variant="body1" sx={{ mr: 1 }}>
                  {progress.percentage}%
                </Typography>
                <Box sx={{ width: '100%' }}>
                  <LinearProgress
                    variant="determinate"
                    value={progress.percentage}
                    sx={{ height: 8, borderRadius: 4 }}
                    color="primary"
                  />
                </Box>
              </Box>

              <Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 2 }}>
                <Typography variant="body2">
                  {t('questions.generatingProgress', {
                    completed: progress.completed,
                    total: progress.total
                  })}
                </Typography>
                <Typography variant="body2" color="success.main" sx={{ fontWeight: 'medium' }}>
                  {t('questions.generatedCount', { count: progress.datasetCount })}
                </Typography>
              </Box>
            </Box>

            <CircularProgress size={60} thickness={4} sx={{ mb: 2 }} />

            <Typography variant="body2" color="text.secondary">
              {t('questions.pleaseWait')}
            </Typography>
          </Paper>
        </Box>
      )}

      <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
        <Typography variant="h4">
          {t('questions.title')} ({questions.total})
        </Typography>
        <Box sx={{ display: 'flex', gap: 2 }}>
          <Button
            variant="outlined"
            color={selectedQuestions.length > 0 ? 'error' : 'primary'}
            startIcon={<DeleteIcon />}
            onClick={handleBatchDeleteQuestions}
            disabled={selectedQuestions.length === 0}
          >
            {t('questions.deleteSelected')}
          </Button>

          <Button variant="contained" startIcon={<AddIcon />} onClick={handleOpenCreateDialog}>
            {t('questions.createQuestion')}
          </Button>
          <Button
            variant="contained"
            startIcon={<AutoFixHighIcon />}
            onClick={handleBatchGenerateAnswers}
            disabled={selectedQuestions.length === 0}
          >
            {t('questions.batchGenerate')}
          </Button>

          <Tooltip
            title={t('questions.autoGenerateDatasetTip', {
              defaultValue: '创建后台批量处理任务：自动查询待生成答案的问题并生成答案'
            })}
          >
            <Button
              variant="outlined"
              color="secondary"
              startIcon={<AutoFixHighIcon />}
              onClick={handleAutoGenerateDatasets}
            >
              {t('questions.autoGenerateDataset', { defaultValue: '自动生成数据集' })}
            </Button>
          </Tooltip>
        </Box>
      </Box>

      <Paper sx={{ mb: 4 }}>
        <Tabs
          value={activeTab}
          onChange={handleTabChange}
          variant="fullWidth"
          indicatorColor="primary"
          sx={{
            borderBottom: 1,
            borderColor: 'divider'
          }}
        >
          <Tab label={t('questions.listView')} />
          <Tab label={t('questions.treeView')} />
        </Tabs>

        <Box sx={{ p: 2 }}>
          <Stack
            direction={{ xs: 'column', sm: 'row' }}
            spacing={2}
            alignItems={{ xs: 'stretch', sm: 'center' }}
            justifyContent="space-between"
          >
            <Box sx={{ display: 'flex', alignItems: 'center' }}>
              <Checkbox
                checked={selectedQuestions.length > 0 && selectedQuestions.length === questions?.total}
                indeterminate={selectedQuestions.length > 0 && selectedQuestions.length < questions?.total}
                onChange={handleSelectAll}
              />
              <Typography variant="body2" sx={{ ml: 1 }}>
                {selectedQuestions.length > 0
                  ? t('questions.selectedCount', { count: selectedQuestions.length })
                  : t('questions.selectAll')}
                (
                {t('questions.totalCount', {
                  count: questions.total
                })}
                )
              </Typography>
            </Box>

            <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
              <TextField
                placeholder={t('questions.searchPlaceholder')}
                variant="outlined"
                size="small"
                fullWidth
                sx={{ width: { xs: '100%', sm: 300 } }}
                value={searchTerm}
                onChange={e => setSearchTerm(e.target.value)}
                InputProps={{
                  startAdornment: (
                    <InputAdornment position="start">
                      <SearchIcon fontSize="small" color="action" />
                    </InputAdornment>
                  )
                }}
              />
              <Select
                value={answerFilter}
                onChange={e => setAnswerFilter(e.target.value)}
                size="small"
                sx={{
                  width: { xs: '100%', sm: 200 },
                  bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'white',
                  borderRadius: '8px',
                  '& .MuiOutlinedInput-notchedOutline': {
                    borderColor: theme.palette.mode === 'dark' ? 'transparent' : 'rgba(0, 0, 0, 0.23)'
                  },
                  '&:hover .MuiOutlinedInput-notchedOutline': {
                    borderColor: theme.palette.mode === 'dark' ? 'transparent' : 'rgba(0, 0, 0, 0.87)'
                  },
                  '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
                    borderColor: 'primary.main'
                  }
                }}
                MenuProps={{
                  PaperProps: {
                    elevation: 2,
                    sx: { mt: 1, borderRadius: 2 }
                  }
                }}
              >
                <MenuItem value="all">{t('questions.filterAll')}</MenuItem>
                <MenuItem value="answered">{t('questions.filterAnswered')}</MenuItem>
                <MenuItem value="unanswered">{t('questions.filterUnanswered')}</MenuItem>
              </Select>
            </Box>
          </Stack>
        </Box>

        <Divider />

        <TabPanel value={activeTab} index={0}>
          <QuestionListView
            questions={questions.data}
            currentPage={currentPage}
            totalQuestions={Math.ceil(questions.total / pageSize)}
            handlePageChange={(_, newPage) => setCurrentPage(newPage)}
            selectedQuestions={selectedQuestions}
            onSelectQuestion={handleSelectQuestion}
            onDeleteQuestion={handleDeleteQuestion}
            onEditQuestion={handleOpenEditDialog}
            refreshQuestions={getQuestionList}
            projectId={projectId}
          />
        </TabPanel>

        <TabPanel value={activeTab} index={1}>
          <QuestionTreeView
            questions={questions.data}
            tags={tags}
            selectedQuestions={selectedQuestions}
            onSelectQuestion={handleSelectQuestion}
            onDeleteQuestion={handleDeleteQuestion}
            onEditQuestion={handleOpenEditDialog}
            projectId={projectId}
            searchTerm={searchTerm}
          />
        </TabPanel>
      </Paper>

      {/* 确认对话框 */}
      <Dialog
        open={confirmDialog.open}
        onClose={() => setConfirmDialog({ ...confirmDialog, open: false })}
        aria-labelledby="alert-dialog-title"
        aria-describedby="alert-dialog-description"
      >
        <DialogTitle id="alert-dialog-title">{confirmDialog.title}</DialogTitle>
        <DialogContent>
          <DialogContentText id="alert-dialog-description">{confirmDialog.content}</DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button onClick={() => setConfirmDialog({ ...confirmDialog, open: false })} color="primary">
            {t('common.cancel')}
          </Button>
          <Button
            onClick={() => {
              setConfirmDialog({ ...confirmDialog, open: false });
              if (confirmDialog.confirmAction) {
                confirmDialog.confirmAction();
              }
            }}
            color="error"
            variant="contained"
            autoFocus
          >
            {t('common.confirmDelete')}
          </Button>
        </DialogActions>
      </Dialog>

      <QuestionEditDialog
        open={editDialogOpen}
        onClose={handleCloseDialog}
        onSubmit={handleSubmitQuestion}
        initialData={editingQuestion}
        tags={tags}
        mode={editMode}
        projectId={projectId}
      />
    </Container>
  );
}

import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// 获取任务详情
export async function GET(request, { params }) {
  try {
    const { projectId, taskId } = params;

    // 验证必填参数
    if (!projectId || !taskId) {
      return NextResponse.json(
        {
          code: 400,
          error: '缺少必要参数'
        },
        { status: 400 }
      );
    }

    // 查询任务详情
    const task = await prisma.task.findUnique({
      where: {
        id: taskId,
        projectId
      }
    });

    if (!task) {
      return NextResponse.json(
        {
          code: 404,
          error: '任务不存在'
        },
        { status: 404 }
      );
    }

    return NextResponse.json({
      code: 0,
      data: task,
      message: '获取任务详情成功'
    });
  } catch (error) {
    console.error('获取任务详情失败:', String(error));
    return NextResponse.json(
      {
        code: 500,
        error: '获取任务详情失败',
        message: error.message
      },
      { status: 500 }
    );
  }
}

// 更新任务状态
export async function PATCH(request, { params }) {
  try {
    const { projectId, taskId } = params;
    const data = await request.json();

    // 验证必填参数
    if (!projectId || !taskId) {
      return NextResponse.json(
        {
          code: 400,
          error: '缺少必要参数'
        },
        { status: 400 }
      );
    }

    // 获取要更新的字段
    const { status, completedCount, totalCount, detail, note, endTime } = data;

    // 构建更新数据
    const updateData = {};

    if (status !== undefined) {
      updateData.status = status;
    }

    if (completedCount !== undefined) {
      updateData.completedCount = completedCount;
    }

    if (totalCount !== undefined) {
      updateData.totalCount = totalCount;
    }

    if (detail !== undefined) {
      updateData.detail = detail;
    }

    if (note !== undefined) {
      updateData.note = note;
    }

    // 如果状态变为已完成、失败或已中断，自动添加结束时间
    if (status === 1 || status === 2 || status === 3) {
      updateData.endTime = endTime || new Date();
    }

    // 更新任务
    const updatedTask = await prisma.task.update({
      where: {
        id: taskId
      },
      data: updateData
    });

    return NextResponse.json({
      code: 0,
      data: updatedTask,
      message: '更新任务状态成功'
    });
  } catch (error) {
    console.error('更新任务状态失败:', String(error));
    return NextResponse.json(
      {
        code: 500,
        error: '更新任务状态失败',
        message: error.message
      },
      { status: 500 }
    );
  }
}

// 删除任务
export async function DELETE(request, { params }) {
  try {
    const { projectId, taskId } = params;

    // 验证必填参数
    if (!projectId || !taskId) {
      return NextResponse.json(
        {
          code: 400,
          error: '缺少必要参数'
        },
        { status: 400 }
      );
    }

    // 删除任务
    await prisma.task.delete({
      where: {
        id: taskId,
        projectId
      }
    });

    return NextResponse.json({
      code: 0,
      message: '删除任务成功'
    });
  } catch (error) {
    console.error('删除任务失败:', String(error));
    return NextResponse.json(
      {
        code: 500,
        error: '删除任务失败',
        message: error.message
      },
      { status: 500 }
    );
  }
}

'use client';

import axios from 'axios';

/**
 * 自动蒸馏服务
 */
class AutoDistillService {
  /**
   * 执行自动蒸馆任务
   * @param {Object} config - 配置信息
   * @param {string} config.projectId - 项目ID
   * @param {string} config.topic - 蒸馆主题
   * @param {number} config.levels - 标签层级
   * @param {number} config.tagsPerLevel - 每层标签数量
   * @param {number} config.questionsPerTag - 每个标签问题数量
   * @param {Object} config.model - 模型信息
   * @param {string} config.language - 语言
   * @param {Function} config.onProgress - 进度回调
   * @param {Function} config.onLog - 日志回调
   * @returns {Promise<void>}
   */
  async executeDistillTask(config) {
    const {
      projectId,
      topic,
      levels,
      tagsPerLevel,
      questionsPerTag,
      model,
      language,
      concurrencyLimit = 5,
      onProgress,
      onLog
    } = config;

    // 项目名称存储，用于整个流程共享
    this.projectName = '';

    try {
      // 初始化进度信息
      if (onProgress) {
        onProgress({
          stage: 'initializing',
          tagsTotal: 0,
          tagsBuilt: 0,
          questionsTotal: 0,
          questionsBuilt: 0,
          datasetsTotal: 0,
          datasetsBuilt: 0
        });
      }

      // 获取项目名称，只需获取一次
      try {
        const projectResponse = await axios.get(`/api/projects/${projectId}`);
        if (projectResponse && projectResponse.data && projectResponse.data.name) {
          this.projectName = projectResponse.data.name;
          this.addLog(onLog, `Using project name "${this.projectName}" as the top-level tag`);
        } else {
          this.projectName = topic; // 如果无法获取项目名称，则使用主题作为默认值
          this.addLog(onLog, `Could not find project name, using topic "${topic}" as the top-level tag`);
        }
      } catch (error) {
        this.projectName = topic; // 出错时使用主题作为默认值
        this.addLog(onLog, `Failed to get project name, using topic "${topic}" instead: ${error.message}`);
      }

      // 添加日志
      this.addLog(
        onLog,
        `Starting to build tag tree for "${topic}", number of levels: ${levels}, tags per level: ${tagsPerLevel}, questions per tag: ${questionsPerTag}`
      );

      // 从根节点开始构建标签树
      await this.buildTagTree({
        projectId,
        topic,
        levels,
        tagsPerLevel,
        model,
        language,
        onProgress,
        onLog
      });

      // 所有标签构建完成后，生成问题
      await this.generateQuestionsForTags({
        projectId,
        levels,
        questionsPerTag,
        model,
        language,
        concurrencyLimit,
        onProgress,
        onLog
      });

      // 生成数据集
      await this.generateDatasetsForQuestions({
        projectId,
        model,
        language,
        concurrencyLimit,
        onProgress,
        onLog
      });

      // 任务完成
      if (onProgress) {
        onProgress({
          stage: 'completed'
        });
      }

      this.addLog(onLog, 'Auto distillation task completed');
    } catch (error) {
      console.error('自动蒸馏任务执行失败:', error);
      this.addLog(onLog, `Task execution error: ${error.message || 'Unknown error'}`);
      throw error;
    }
  }

  /**
   * 构建标签树
   * @param {Object} config - 配置信息
   * @param {string} config.projectId - 项目ID
   * @param {string} config.topic - 蒸馆主题
   * @param {number} config.levels - 标签层级
   * @param {number} config.tagsPerLevel - 每层标签数量
   * @param {Object} config.model - 模型信息
   * @param {string} config.language - 语言
   * @param {Function} config.onProgress - 进度回调
   * @param {Function} config.onLog - 日志回调
   * @returns {Promise<void>}
   */
  async buildTagTree(config) {
    const { projectId, topic, levels, tagsPerLevel, model, language, onProgress, onLog } = config;

    // 使用已经获取的项目名称，如果未获取到，则使用主题
    const projectName = this.projectName || topic;

    // 递归构建标签树
    const buildTagsForLevel = async (parentTag = null, parentTagPath = '', level = 1) => {
      // 设置当前阶段
      if (onProgress) {
        onProgress({
          stage: `level${level}`
        });
      }

      // 如果已经达到目标层级，停止递归
      if (level > levels) {
        return;
      }

      // 获取当前级别的标签
      let currentLevelTags = [];
      try {
        // 获取所有标签，然后根据父标签ID进行过滤
        const response = await axios.get(`/api/projects/${projectId}/distill/tags/all`);

        // 如果有父标签，过滤出当前父标签下的子标签
        if (parentTag) {
          currentLevelTags = response.data.filter(tag => tag.parentId === parentTag.id);
        } else {
          // 如果没有父标签，则获取所有根标签（没有parentId的标签）
          currentLevelTags = response.data.filter(tag => !tag.parentId);
        }
      } catch (error) {
        console.error(`获取${level}级标签失败:`, error);
        this.addLog(onLog, `Failed to get ${level} level tags: ${error.message}`);
        return;
      }

      // 计算需要创建的标签数量
      const targetCount = tagsPerLevel;
      const currentCount = currentLevelTags.length;
      const needToCreate = Math.max(0, targetCount - currentCount);

      // 如果需要创建标签
      if (needToCreate > 0) {
        // 如果是第一级标签，使用配置中的主题名称
        const parentTagName = level === 1 ? topic : parentTag?.label || '';

        this.addLog(onLog, `Tag tree level ${level}: Creating ${tagsPerLevel} subtags for "${parentTagName}"...`);

        // 构建标签路径，确保以项目名称开头
        let tagPathWithProjectName;
        if (level === 1) {
          // 如果是第一级，使用项目名称
          tagPathWithProjectName = projectName;
        } else {
          // 如果不是第一级，确保标签路径以项目名称开头
          if (!parentTagPath) {
            tagPathWithProjectName = projectName;
          } else if (!parentTagPath.startsWith(projectName)) {
            tagPathWithProjectName = `${projectName} > ${parentTagPath}`;
          } else {
            tagPathWithProjectName = parentTagPath;
          }
        }

        try {
          const response = await axios.post(`/api/projects/${projectId}/distill/tags`, {
            parentTag: parentTagName,
            parentTagId: parentTag ? parentTag.id : null,
            tagPath: tagPathWithProjectName || parentTagName,
            count: needToCreate,
            model,
            language
          });

          // 更新构建的标签数量
          if (onProgress) {
            onProgress({
              tagsBuilt: response.data.length,
              updateType: 'increment'
            });
          }

          // 添加日志
          this.addLog(
            onLog,
            `Successfully created ${response.data.length} tags: ${response.data.map(tag => tag.label).join(', ')}`
          );

          // 将新创建的标签添加到当前级别标签列表中
          currentLevelTags = [...currentLevelTags, ...response.data];
        } catch (error) {
          console.error(`创建${level}级标签失败:`, error);
          this.addLog(onLog, `Failed to create ${level} level tags: ${error.message || 'Unknown error'}`);
        }
      }

      // 如果不是最后一层，继续递归构建下一层标签
      if (level < levels) {
        for (const tag of currentLevelTags) {
          // 构建标签路径，确保以项目名称开头
          let tagPath;
          if (parentTagPath) {
            tagPath = `${parentTagPath} > ${tag.label}`;
          } else {
            tagPath = `${projectName} > ${tag.label}`;
          }

          // 递归构建子标签
          await buildTagsForLevel(tag, tagPath, level + 1);
        }
      }
    };

    // 获取叶子节点总数，更新进度条
    const leafTags = Math.pow(tagsPerLevel, levels);
    if (onProgress) {
      onProgress({
        tagsTotal: leafTags
      });
    }

    // 从第一层开始构建标签树
    await buildTagsForLevel();
  }

  /**
   * 为标签生成问题
   * @param {Object} config - 配置信息
   * @param {string} config.projectId - 项目ID
   * @param {number} config.levels - 标签层级
   * @param {number} config.questionsPerTag - 每个标签问题数量
   * @param {Object} config.model - 模型信息
   * @param {string} config.language - 语言
   * @param {Function} config.onProgress - 进度回调
   * @param {Function} config.onLog - 日志回调
   * @returns {Promise<void>}
   */
  async generateQuestionsForTags(config) {
    const { projectId, levels, questionsPerTag, model, language, concurrencyLimit = 5, onProgress, onLog } = config;

    // 设置当前阶段
    if (onProgress) {
      onProgress({
        stage: 'questions'
      });
    }

    this.addLog(onLog, 'Tag tree built, starting to generate questions for leaf tags...');

    try {
      // 获取所有标签
      const response = await axios.get(`/api/projects/${projectId}/distill/tags/all`);
      const allTags = response.data;

      // 找出所有叶子标签(没有子标签的标签)
      const leafTags = [];

      // 创建一个映射表，记录每个标签的子标签
      const childrenMap = {};
      allTags.forEach(tag => {
        if (tag.parentId) {
          if (!childrenMap[tag.parentId]) {
            childrenMap[tag.parentId] = [];
          }
          childrenMap[tag.parentId].push(tag);
        }
      });

      // 找出所有叶子标签
      allTags.forEach(tag => {
        // 如果没有子标签，并且深度是最大层级，则为叶子标签
        if (!childrenMap[tag.id] && this.getTagDepth(tag, allTags) === levels) {
          leafTags.push(tag);
        }
      });

      this.addLog(onLog, `Found ${leafTags.length} leaf tags, starting to generate questions...`);

      // 获取所有问题
      const questionsResponse = await axios.get(`/api/projects/${projectId}/questions/tree?isDistill=true`);
      const allQuestions = questionsResponse.data;

      // 更新总问题数量
      const totalQuestionsToGenerate = leafTags.length * questionsPerTag;
      if (onProgress) {
        onProgress({
          questionsTotal: totalQuestionsToGenerate
        });
      }

      // 准备并发任务
      const generateQuestionTasks = [];
      const processedTags = [];

      // 准备所有需要生成问题的叶子标签任务
      for (const tag of leafTags) {
        // 获取标签路径
        const tagPath = this.getTagPath(tag, allTags);

        // 计算已有问题数量
        const existingQuestions = allQuestions.filter(q => q.label === tag.label);
        const needToCreate = Math.max(0, questionsPerTag - existingQuestions.length);

        if (needToCreate > 0) {
          // 只添加需要生成问题的标签任务
          generateQuestionTasks.push({
            tag,
            tagPath,
            needToCreate
          });

          this.addLog(onLog, `Preparing to generate ${needToCreate} questions for tag "${tag.label}"...`);
        } else {
          this.addLog(
            onLog,
            `Tag "${tag.label}" already has ${existingQuestions.length} questions, no need to generate new questions`
          );
        }
      }

      // 分批执行生成问题任务，控制并发数
      this.addLog(
        onLog,
        `Total ${generateQuestionTasks.length} tags need questions, concurrency limit: ${concurrencyLimit}`
      );

      // 使用分组批量处理
      for (let i = 0; i < generateQuestionTasks.length; i += concurrencyLimit) {
        const batch = generateQuestionTasks.slice(i, i + concurrencyLimit);

        // 并行处理批次任务
        await Promise.all(
          batch.map(async task => {
            const { tag, tagPath, needToCreate } = task;

            this.addLog(onLog, `Generating ${needToCreate} questions for tag "${tag.label}"...`);

            try {
              const response = await axios.post(`/api/projects/${projectId}/distill/questions`, {
                tagPath,
                currentTag: tag.label,
                tagId: tag.id,
                count: needToCreate,
                model,
                language
              });

              // 更新生成的问题数量
              if (onProgress) {
                onProgress({
                  questionsBuilt: response.data.length,
                  updateType: 'increment'
                });
              }

              const questions = response.data
                .map(r => r.question || r.content)
                .slice(0, 3)
                .join('\n'); // 只显示前3个问题以避免日志过长
              this.addLog(onLog, `Successfully generated ${response.data.length} questions for tag "${tag.label}"`);
            } catch (error) {
              console.error(`为标签 "${tag.label}" 生成问题失败:`, error);
              this.addLog(
                onLog,
                `Failed to generate questions for tag "${tag.label}": ${error.message || 'Unknown error'}`
              );
            }
          })
        );

        // 每完成一批，输出一次进度日志
        this.addLog(
          onLog,
          `Completed batch ${Math.min(i + concurrencyLimit, generateQuestionTasks.length)}/${generateQuestionTasks.length} of question generation`
        );
      }
    } catch (error) {
      console.error('获取标签失败:', error);
      this.addLog(onLog, `Failed to get tags: ${error.message || 'Unknown error'}`);
    }
  }

  /**
   * 为问题生成数据集
   * @param {Object} config - 配置信息
   * @param {string} config.projectId - 项目ID
   * @param {Object} config.model - 模型信息
   * @param {string} config.language - 语言
   * @param {Function} config.onProgress - 进度回调
   * @param {Function} config.onLog - 日志回调
   * @returns {Promise<void>}
   */
  async generateDatasetsForQuestions(config) {
    const { projectId, model, language, concurrencyLimit = 5, onProgress, onLog } = config;

    // 设置当前阶段
    if (onProgress) {
      onProgress({
        stage: 'datasets'
      });
    }

    this.addLog(onLog, 'Question generation completed, starting to generate answers...');

    try {
      // 获取所有问题
      const response = await axios.get(`/api/projects/${projectId}/questions/tree?isDistill=true`);
      const allQuestions = response.data;

      // 找出未回答的问题
      const unansweredQuestions = allQuestions.filter(q => !q.answered);
      const answeredQuestions = allQuestions.filter(q => q.answered);

      // 更新总数据集数量和已生成数量
      if (onProgress) {
        onProgress({
          datasetsTotal: allQuestions.length, // 总数据集数量应为总问题数量
          datasetsBuilt: answeredQuestions.length // 已生成的数据集数量即已回答的问题数量
        });
      }

      this.addLog(onLog, `Found ${unansweredQuestions.length} unanswered questions, preparing to generate answers...`);
      this.addLog(onLog, `Dataset generation concurrency limit: ${concurrencyLimit}`);

      // 分批处理未回答的问题，控制并发数
      for (let i = 0; i < unansweredQuestions.length; i += concurrencyLimit) {
        const batch = unansweredQuestions.slice(i, i + concurrencyLimit);

        // 并行处理批次任务
        await Promise.all(
          batch.map(async question => {
            const questionContent = `${question.label} 下的问题ID:${question.id}`;
            this.addLog(onLog, `Generating answer for "${questionContent}"...`);

            try {
              // 调用生成数据集的函数
              await this.generateSingleDataset({
                projectId,
                questionId: question.id,
                questionInfo: question,
                model,
                language
              });

              // 更新生成的数据集数量
              if (onProgress) {
                onProgress({
                  datasetsBuilt: 1,
                  updateType: 'increment'
                });
              }

              this.addLog(onLog, `Successfully generated answer for question "${questionContent}"`);
            } catch (error) {
              console.error(`Failed to generate dataset for question "${question.id}":`, error);
              this.addLog(
                onLog,
                `Failed to generate answer for question "${questionContent}": ${error.message || 'Unknown error'}`
              );
            }
          })
        );

        // 每完成一批，输出一次进度日志
        this.addLog(
          onLog,
          `Completed batch ${Math.min(i + concurrencyLimit, unansweredQuestions.length)}/${unansweredQuestions.length} of dataset generation`
        );
      }
    } catch (error) {
      console.error('Failed to get questions:', error);
      this.addLog(onLog, `Failed to get questions: ${error.message || 'Unknown error'}`);
    }
  }

  /**
   * 生成单个数据集
   * @param {Object} config - 配置信息
   * @param {string} config.projectId - 项目ID
   * @param {string} config.questionId - 问题ID
   * @param {Object} config.questionInfo - 问题信息
   * @param {Object} config.model - 模型信息
   * @param {string} config.language - 语言
   * @returns {Promise<Object>} - 数据集信息
   */
  async generateSingleDataset({ projectId, questionId, questionInfo, model, language }) {
    try {
      // 构建请求参数
      const params = {
        model,
        language: language || 'zh-CN'
      };

      // 获取问题信息
      let question = questionInfo;
      if (!question) {
        const response = await axios.get(`/api/projects/${projectId}/questions/${questionId}`);
        question = response.data;
      }

      // 生成数据集
      const response = await axios.post(`/api/projects/${projectId}/datasets`, {
        projectId,
        questionId,
        model,
        language: language || 'zh-CN'
      });

      return response.data;
    } catch (error) {
      console.error('Failed to generate dataset:', error);
      throw new Error(`Failed to generate dataset: ${error.message}`);
    }
  }

  /**
   * 获取标签深度
   * @param {Object} tag - 标签信息
   * @param {Array} allTags - 所有标签
   * @returns {number} - 标签深度
   */
  getTagDepth(tag, allTags) {
    let depth = 1;
    let currentTag = tag;

    while (currentTag.parentId) {
      depth++;
      currentTag = allTags.find(t => t.id === currentTag.parentId);
      if (!currentTag) break;
    }

    return depth;
  }

  /**
   * 获取标签路径
   * @param {Object} tag - 标签信息
   * @param {Array} allTags - 所有标签
   * @returns {string} - 标签路径
   */
  /**
   * 获取标签路径，确保始终以项目名称开头
   * @param {Object} tag - 标签对象
   * @param {Array} allTags - 所有标签数组
   * @returns {string} 标签路径
   */
  getTagPath(tag, allTags) {
    // 使用已经获取的项目名称
    const projectName = this.projectName || '';

    // 构建标签路径
    const path = [];
    let currentTag = tag;

    while (currentTag) {
      path.unshift(currentTag.label);
      if (currentTag.parentId) {
        currentTag = allTags.find(t => t.id === currentTag.parentId);
      } else {
        currentTag = null;
      }
    }

    // 确保路径以项目名称开头
    if (projectName && path.length > 0 && path[0] !== projectName) {
      path.unshift(projectName);
    }

    return path.join(' > ');
  }

  /**
   * 添加日志
   * @param {Function} onLog - 日志回调
   * @param {string} message - 日志消息
   */
  addLog(onLog, message) {
    if (onLog && typeof onLog === 'function') {
      onLog(message);
    }
  }
}

export const autoDistillService = new AutoDistillService();
export default autoDistillService;

import { NextResponse } from 'next/server';
import { getProject } from '@/lib/db/projects';
import { getDatasets } from '@/lib/db/datasets';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { uploadFiles, createRepo, checkRepoAccess } from '@huggingface/hub';

// 上传数据集到 HuggingFace
export async function POST(request, { params }) {
  try {
    const projectId = params.projectId;
    const {
      token,
      datasetName,
      isPrivate,
      formatType,
      systemPrompt,
      confirmedOnly,
      includeCOT,
      fileFormat,
      customFields
    } = await request.json();

    // 获取项目信息
    const project = await getProject(projectId);
    if (!project) {
      return NextResponse.json({ error: '项目不存在' }, { status: 404 });
    }

    // 获取数据集问题
    const questions = await getDatasets(projectId, confirmedOnly);
    if (!questions || questions.length === 0) {
      return NextResponse.json({ error: '没有可用的数据集问题' }, { status: 400 });
    }

    // 格式化数据集
    const formattedData = formatDataset(questions, formatType, systemPrompt, includeCOT, customFields);

    // 创建临时目录
    const tempDir = path.join(os.tmpdir(), `hf-upload-${projectId}-${Date.now()}`);
    fs.mkdirSync(tempDir, { recursive: true });

    // 创建数据集文件
    const datasetFilePath = path.join(tempDir, `dataset.${fileFormat}`);
    if (fileFormat === 'json') {
      fs.writeFileSync(datasetFilePath, JSON.stringify(formattedData, null, 2));
    } else if (fileFormat === 'jsonl') {
      const jsonlContent = formattedData.map(item => JSON.stringify(item)).join('\n');
      fs.writeFileSync(datasetFilePath, jsonlContent);
    } else if (fileFormat === 'csv') {
      const csvContent = convertToCSV(formattedData);
      fs.writeFileSync(datasetFilePath, csvContent);
    }

    // 创建 README.md 文件
    const readmePath = path.join(tempDir, 'README.md');
    const readmeContent = generateReadme(project.name, project.description, formatType);
    fs.writeFileSync(readmePath, readmeContent);

    // 使用 Hugging Face REST API 上传数据集
    const visibility = isPrivate ? 'private' : 'public';

    try {
      // 准备仓库配置
      const repo = { type: 'dataset', name: datasetName };

      // 检查仓库是否存在
      let repoExists = true;
      try {
        await checkRepoAccess({ repo, accessToken: token });
        console.log(`Repository ${datasetName} exists, continuing to upload files`);
      } catch (error) {
        // If error code is 404, the repository does not exist
        if (error.statusCode === 404) {
          repoExists = false;
          console.log(`Repository ${datasetName} does not exist, preparing to create`);
        } else {
          // Other errors (e.g., permission errors)
          throw new Error(`Failed to check repository access: ${error.message}`);
        }
      }

      // If the repository does not exist, create a new one
      if (!repoExists) {
        try {
          await createRepo({
            repo,
            accessToken: token,
            private: isPrivate,
            license: 'mit',
            description: project.description || 'Dataset created with Easy Dataset'
          });
          console.log(`Successfully created dataset repository: ${datasetName}`);
        } catch (error) {
          throw new Error(`Failed to create dataset repository: ${error.message}`);
        }
      }

      // 2. 上传数据集文件
      await uploadFile(token, datasetName, datasetFilePath, `dataset.${fileFormat}`);

      // 3. 上传 README.md
      await uploadFile(token, datasetName, readmePath, 'README.md');
    } catch (error) {
      console.error('Upload to HuggingFace Failed:', String(error));
      return NextResponse.json({ error: `Upload Error: ${error.message}` }, { status: 500 });
    }

    // 清理临时目录
    fs.rmSync(tempDir, { recursive: true, force: true });

    // 返回成功信息
    const datasetUrl = `https://huggingface.co/datasets/${datasetName}`;
    return NextResponse.json({
      success: true,
      message: 'Upload successfully HuggingFace',
      url: datasetUrl
    });
  } catch (error) {
    console.error('Upload Faile:', String(error));
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

// 格式化数据集
function formatDataset(questions, formatType, systemPrompt, includeCOT, customFields) {
  if (formatType === 'alpaca') {
    return questions.map(q => {
      const item = {
        instruction: q.question,
        input: '',
        output: includeCOT && q.cot ? `${q.cot}\n\n${q.answer}` : q.answer
      };

      if (systemPrompt) {
        item.system = systemPrompt;
      }

      return item;
    });
  } else if (formatType === 'sharegpt') {
    return questions.map(q => {
      const messages = [];

      if (systemPrompt) {
        messages.push({
          role: 'system',
          content: systemPrompt
        });
      }

      messages.push({
        role: 'user',
        content: q.question
      });

      messages.push({
        role: 'assistant',
        content: includeCOT && q.cot ? `${q.cot}\n\n${q.answer}` : q.answer
      });

      return { messages };
    });
  } else if (formatType === 'custom' && customFields) {
    return questions.map(q => {
      const item = {
        [customFields.questionField]: q.question,
        [customFields.answerField]: q.answer
      };

      if (includeCOT && q.cot) {
        item[customFields.cotField] = q.cot;
      }

      if (customFields.includeLabels && q.labels) {
        item.labels = q.labels;
      }

      if (customFields.includeChunk && q.chunkId) {
        item.chunkId = q.chunkId;
      }

      return item;
    });
  }

  // 默认返回 alpaca 格式
  return questions.map(q => ({
    instruction: q.question,
    output: includeCOT && q.cot ? `${q.cot}\n\n${q.answer}` : q.answer
  }));
}

// 将数据转换为 CSV 格式
function convertToCSV(data) {
  if (!data || data.length === 0) return '';

  const headers = Object.keys(data[0]);
  const headerRow = headers.join(',');

  const rows = data.map(item => {
    return headers
      .map(header => {
        const value = item[header];
        if (typeof value === 'string') {
          // 处理字符串中的逗号和引号
          return `"${value.replace(/"/g, '""')}"`;
        } else if (Array.isArray(value)) {
          return `"${JSON.stringify(value).replace(/"/g, '""')}"`;
        } else if (typeof value === 'object' && value !== null) {
          return `"${JSON.stringify(value).replace(/"/g, '""')}"`;
        }
        return value;
      })
      .join(',');
  });

  return [headerRow, ...rows].join('\n');
}

// 使用 @huggingface/hub 包上传文件到 HuggingFace
async function uploadFile(token, datasetName, filePath, destFileName) {
  try {
    // 准备仓库配置
    const repo = { type: 'dataset', name: datasetName };

    // 创建文件 URL
    const fileUrl = new URL(`file://${filePath}`);

    // 使用 @huggingface/hub 包上传文件
    await uploadFiles({
      repo,
      accessToken: token,
      files: [
        {
          path: destFileName,
          content: fileUrl
        }
      ],
      commitTitle: `Upload ${destFileName}`,
      commitDescription: `Files uploaded using Easy Dataset`
    });

    return { success: true };
  } catch (error) {
    console.error(`File ${destFileName} Upload Error:`, String(error));
    throw error;
  }
}

// Generate README.md file
function generateReadme(projectName, projectDescription, formatType) {
  return `# ${projectName}

## Description
${projectDescription || 'This dataset was created using the Easy Dataset tool.'}

## Format
This dataset is in ${formatType} format.

## Creation Method
This dataset was created using the [Easy Dataset](https://github.com/ConardLi/easy-dataset) tool.

> Easy Dataset is a specialized application designed to streamline the creation of fine-tuning datasets for Large Language Models (LLMs). It offers an intuitive interface for uploading domain-specific files, intelligently splitting content, generating questions, and producing high-quality training data for model fine-tuning.

`;
}

'use server';

import fs from 'fs';
import path from 'path';
import { getProjectRoot, readJsonFile } from './base';
import { DEFAULT_SETTINGS } from '@/constant/setting';
import { db } from '@/lib/db/index';
import { nanoid } from 'nanoid';

// 创建新项目
export async function createProject(projectData) {
  try {
    let projectId = nanoid(12);
    const projectRoot = await getProjectRoot();
    const projectDir = path.join(projectRoot, projectId);
    // 创建项目目录
    await fs.promises.mkdir(projectDir, { recursive: true });
    // 创建子目录
    await fs.promises.mkdir(path.join(projectDir, 'files'), { recursive: true }); // 原始文件
    return await db.projects.create({
      data: {
        id: projectId,
        name: projectData.name,
        description: projectData.description
      }
    });
  } catch (error) {
    console.error('Failed to create project in database');
    throw error;
  }
}

export async function isExistByName(name) {
  try {
    const count = await db.projects.count({
      where: {
        name: name
      }
    });
    return count > 0;
  } catch (error) {
    console.error('Failed to get project by name in database');
    throw error;
  }
}

// 获取所有项目
export async function getProjects() {
  try {
    return await db.projects.findMany({
      include: {
        _count: {
          select: {
            Datasets: true,
            Questions: true
          }
        }
      },
      orderBy: {
        createAt: 'desc'
      }
    });
  } catch (error) {
    console.error('Failed to get projects in database');
    throw error;
  }
}

// 获取项目详情
export async function getProject(projectId) {
  try {
    return await db.projects.findUnique({ where: { id: projectId } });
  } catch (error) {
    console.error('Failed to get project by id in database');
    throw error;
  }
}

// 更新项目配置
export async function updateProject(projectId, projectData) {
  try {
    delete projectData.projectId;
    return await db.projects.update({
      where: { id: projectId },
      data: { ...projectData }
    });
  } catch (error) {
    console.error('Failed to update project in database');
    throw error;
  }
}

// 删除项目
export async function deleteProject(projectId) {
  try {
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);
    await db.projects.delete({ where: { id: projectId } });
    if (fs.existsSync(projectPath)) {
      await fs.promises.rm(projectPath, { recursive: true });
    }
    return true;
  } catch (error) {
    return false;
  }
}

// 获取任务配置
export async function getTaskConfig(projectId) {
  const projectRoot = await getProjectRoot();
  const projectPath = path.join(projectRoot, projectId);
  const taskConfigPath = path.join(projectPath, 'task-config.json');
  const taskData = await readJsonFile(taskConfigPath);
  if (!taskData) {
    return DEFAULT_SETTINGS;
  }
  return taskData;
}

'use client';

import { Box, Typography, keyframes, Paper } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { handleLongFileName } from '@/lib/file/file-process';
import { useState, useEffect } from 'react';

// 定义动画效果
const pulse = keyframes`
  0% {
    box-shadow: 0 0 0 0 rgba(32, 76, 255, 0.2);
  }
  70% {
    box-shadow: 0 0 0 15px rgba(32, 76, 255, 0);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(32, 76, 255, 0);
  }
`;

const rotateAnimation = keyframes`
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
`;

const shimmer = keyframes`
  0% { background-position: -200% 0; }
  100% { background-position: 200% 0; }
`;

/**
 * 文件处理进度展示组件 - 美化版
 *
 * @param {Object} props
 * @param {Object} props.fileTask - 文件处理任务信息
 */
export default function FileLoadingProgress({ fileTask }) {
  const { t } = useTranslation();
  const [animationStep, setAnimationStep] = useState(0);

  // 创建动态效果
  useEffect(() => {
    const interval = setInterval(() => {
      setAnimationStep(prev => (prev + 1) % 4);
    }, 600);
    return () => clearInterval(interval);
  }, []);

  if (!fileTask) {
    return null;
  }

  const pageProgress = (fileTask.current.processedPage / fileTask.current.totalPage) * 100;
  const filesProgress = (fileTask.processedFiles / fileTask.totalFiles) * 100;

  // 生成进度指示器文本
  const getProgressIndicator = () => {
    const dots = '.';
    return dots.repeat(animationStep + 1);
  };

  return (
    <Paper
      elevation={3}
      sx={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        height: 'auto',
        minHeight: '25vh',
        width: '80%',
        maxWidth: '600px',
        margin: '0 auto',
        padding: 4,
        borderRadius: 3,
        position: 'relative',
        overflow: 'hidden',
        background: 'linear-gradient(45deg, #f9f9f9 0%, #ffffff 100%)',
        animation: `${pulse} 2s infinite`
      }}
    >
      {/* 背景动画元素 */}
      <Box
        sx={{
          position: 'absolute',
          top: '-50%',
          left: '-50%',
          width: '200%',
          height: '200%',
          background: 'radial-gradient(circle, rgba(32,76,255,0.05) 0%, rgba(255,255,255,0) 70%)',
          animation: `${rotateAnimation} 15s linear infinite`,
          zIndex: 0
        }}
      />

      {/* 主标题 */}
      <Typography
        variant="h5"
        fontWeight="bold"
        sx={{
          mb: 3,
          position: 'relative',
          zIndex: 1,
          background: 'linear-gradient(90deg, #3a7bd5 0%, #00d2ff 100%)',
          backgroundClip: 'text',
          textFillColor: 'transparent',
          WebkitBackgroundClip: 'text',
          WebkitTextFillColor: 'transparent'
        }}
      >
        {t('textSplit.pdfProcessingLoading')}
        {getProgressIndicator()}
      </Typography>

      {/* 处理进度显示区域 */}
      <Box sx={{ width: '90%', mt: 2, mb: 3, position: 'relative', zIndex: 1 }}>
        {/* 当前文件进度 */}
        <ProgressSection
          label={t('textSplit.pdfPageProcessStatus', {
            fileName: handleLongFileName(fileTask.current.fileName),
            total: fileTask.current.totalPage,
            completed: fileTask.current.processedPage
          })}
          progress={pageProgress}
          color="#3a7bd5"
        />

        {/* 总文件进度 */}
        <ProgressSection
          label={t('textSplit.pdfProcessStatus', {
            total: fileTask.totalFiles,
            completed: fileTask.processedFiles
          })}
          progress={filesProgress}
          color="#00d2ff"
          mt={3}
        />
      </Box>
    </Paper>
  );
}

/**
 * 进度条区域组件
 */
function ProgressSection({ label, progress, color, mt = 0 }) {
  return (
    <Box sx={{ mt }}>
      <Box
        sx={{
          display: 'flex',
          justifyContent: 'space-between',
          mb: 1,
          alignItems: 'center'
        }}
      >
        <Typography
          variant="body2"
          fontWeight="medium"
          sx={{
            color: 'text.primary',
            fontSize: '0.9rem'
          }}
        >
          {label}
        </Typography>
        <Typography
          variant="h6"
          fontWeight="bold"
          sx={{
            color,
            fontSize: '1.1rem'
          }}
        >
          {Math.round(progress)}%
        </Typography>
      </Box>

      {/* 自定义进度条 */}
      <Box
        sx={{
          height: 10,
          borderRadius: 5,
          background: '#f0f0f0',
          position: 'relative',
          overflow: 'hidden',
          boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.1)'
        }}
      >
        <Box
          sx={{
            position: 'absolute',
            top: 0,
            left: 0,
            height: '100%',
            width: `${progress}%`,
            borderRadius: 5,
            background: `linear-gradient(90deg, ${color} 0%, ${color}80 100%)`,
            transition: 'width 0.5s ease',
            backgroundSize: '200% 100%',
            animation: `${shimmer} 2s infinite linear`
          }}
        />
      </Box>
    </Box>
  );
}

import i18n from '@/lib/i18n';
import { request } from '@/lib/util/request';

/**
 * 上传文件
 * @param {File} file 文件
 * @param {string} projectId 项目ID
 * @param {string} fileContent 文件内容
 * @param {string} fileName 文件名
 * @param {function} t 国际化函数
 * @returns
 */
export async function uploadFile({ file, projectId, fileContent, fileName, t }) {
  return await request(`/api/projects/${projectId}/files`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/octet-stream',
      'x-file-name': encodeURIComponent(fileName)
    },
    body: file.name.endsWith('.docx') ? new TextEncoder().encode(fileContent) : fileContent,
    errMsg: t('textSplit.uploadFailed')
  });
}

/**
 * 删除文件
 * @param {Object} fileToDelete 文件信息
 * @param {string} projectId 项目ID
 * @param {string} domainTreeActionType 域树处理方式
 * @param {Object} modelInfo 模型信息
 * @returns
 */
export async function deleteFile({ fileToDelete, projectId, domainTreeActionType, modelInfo }) {
  return await request(
    `/api/projects/${projectId}/files?fileId=${fileToDelete.fileId}&domainTreeAction=${domainTreeActionType || 'keep'}`,
    {
      method: 'DELETE',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: modelInfo,
        language: i18n.language === 'zh-CN' ? '中文' : 'en'
      })
    }
  );
}

/**
 * 获取文件列表
 * @param {string} projectId 项目ID
 * @param {number} page 页码
 * @param {number} size 每页大小
 * @param {string} fileName 搜索文件名（可选）
 */
export async function getFiles({ projectId, page, size, fileName }) {
  const params = new URLSearchParams({
    page: page.toString(),
    pageSize: size.toString()
  });

  if (fileName && fileName.trim()) {
    params.append('fileName', fileName.trim());
  }

  return await request(`/api/projects/${projectId}/files?${params}`);
}

/**
 * 领域树处理模块
 * 用于处理领域树的生成、修订和管理
 */
const LLMClient = require('../llm/core/index');
const getLabelPrompt = require('../llm/prompts/label');
const getLabelEnPrompt = require('../llm/prompts/labelEn');
const getLabelRevisePrompt = require('../llm/prompts/labelRevise');
const getLabelReviseEnPrompt = require('../llm/prompts/labelReviseEn');
const { getProjectTocs } = require('../file/text-splitter');
const { getTags, batchSaveTags } = require('../db/tags');
const { extractJsonFromLLMOutput } = require('../llm/common/util');
const { filterDomainTree } = require('./file');

/**
 * 处理领域树生成或更新
 * @param {Object} options - 配置选项
 * @param {string} options.projectId - 项目ID
 * @param {string} options.action - 操作类型: 'rebuild', 'revise', 'keep'
 * @param {string} options.toc - 所有文档的目录结构
 * @param {Object} options.model - 使用的模型信息
 * @param {string} options.language - 语言: 'en' 或 '中文'
 * @param {string} options.fileName - 文件名（用于新增文件时获取内容）
 * @param {string} options.deletedContent - 被删除的文件内容（用于删除文件时）
 * @param {Object} options.project - 项目信息，包含 globalPrompt 和 domainTreePrompt
 * @returns {Promise<Array>} 生成的领域树标签
 */
async function handleDomainTree({
  projectId,
  action = 'rebuild',
  allToc,
  newToc,
  model,
  language = '中文',
  deleteToc = null,
  project
}) {
  // 如果是保持不变，直接返回现有标签
  if (action === 'keep') {
    console.log(`[${projectId}] Using existing domain tree`);
    return await getTags(projectId);
  }

  const isEnglish = language === 'en';
  const { globalPrompt, domainTreePrompt } = project || {};

  try {
    if (!allToc) {
      allToc = await getProjectTocs(projectId);
    }

    const llmClient = new LLMClient(model);
    let tags, prompt, response;
    // 重建领域树
    if (action === 'rebuild') {
      console.log(`[${projectId}] Rebuilding domain tree`);
      const promptFunc = isEnglish ? getLabelEnPrompt : getLabelPrompt;
      prompt = promptFunc({ text: allToc, globalPrompt, domainTreePrompt });
      console.log('rebuild', prompt);
      response = await llmClient.getResponse(prompt);
      tags = extractJsonFromLLMOutput(response);
      console.log('rebuild tags', tags);
    }
    // 修订领域树
    else if (action === 'revise') {
      console.log(`[${projectId}] Revising domain tree`);
      // 获取现有的领域树
      const existingTags = await getTags(projectId);

      if (!existingTags || existingTags.length === 0) {
        // 如果没有现有领域树，就像重建一样处理
        const promptFunc = isEnglish ? getLabelEnPrompt : getLabelPrompt;
        prompt = promptFunc({ text: allToc, globalPrompt, domainTreePrompt });
      } else {
        // 增量更新领域树的逻辑
        const promptFunc = isEnglish ? getLabelReviseEnPrompt : getLabelRevisePrompt;

        prompt = promptFunc({
          text: allToc,
          existingTags: filterDomainTree(existingTags),
          newContent: newToc,
          deletedContent: deleteToc,
          globalPrompt,
          domainTreePrompt
        });
      }

      // console.log('revise', prompt);

      response = await llmClient.getResponse(prompt);
      tags = extractJsonFromLLMOutput(response);

      // console.log('revise tags', tags);
    }

    // 保存领域树标签（如果生成成功）
    if (tags && tags.length > 0 && action !== 'keep') {
      await batchSaveTags(projectId, tags);
    } else if (!tags && action !== 'keep') {
      console.error(`[${projectId}] Failed to generate domain tree tags`);
    }

    return tags;
  } catch (error) {
    console.error(`[${projectId}] Error handling domain tree: ${error.message}`);
    throw error;
  }
}

module.exports = {
  handleDomainTree
};

import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

/**
 * 批量编辑文本块内容
 * POST /api/projects/[projectId]/chunks/batch-edit
 */
export async function POST(request, { params }) {
  try {
    const { projectId } = params;
    const body = await request.json();
    const { position, content, chunkIds } = body;

    // 验证参数
    if (!position || !content || !chunkIds || !Array.isArray(chunkIds) || chunkIds.length === 0) {
      return NextResponse.json({ error: 'Missing required parameters: position, content, chunkIds' }, { status: 400 });
    }

    if (!['start', 'end'].includes(position)) {
      return NextResponse.json({ error: 'Position must be "start" or "end"' }, { status: 400 });
    }

    // 验证项目权限（获取要编辑的文本块）
    const chunksToUpdate = await prisma.chunks.findMany({
      where: {
        id: { in: chunkIds },
        projectId: projectId
      },
      select: {
        id: true,
        content: true,
        name: true
      }
    });

    if (chunksToUpdate.length === 0) {
      return NextResponse.json({ error: 'Not found' }, { status: 404 });
    }

    if (chunksToUpdate.length !== chunkIds.length) {
      return NextResponse.json({ error: 'Some chunks not found' }, { status: 400 });
    }

    // 准备更新数据
    const updates = chunksToUpdate.map(chunk => {
      let newContent;

      if (position === 'start') {
        // 在开头添加内容
        newContent = content + '\n\n' + chunk.content;
      } else {
        // 在结尾添加内容
        newContent = chunk.content + '\n\n' + content;
      }

      return {
        where: { id: chunk.id },
        data: {
          content: newContent,
          size: newContent.length,
          updateAt: new Date()
        }
      };
    });

    async function processBatches(items, batchSize, processFn) {
      const results = [];
      for (let i = 0; i < items.length; i += batchSize) {
        const batch = items.slice(i, i + batchSize);
        const batchResults = await Promise.all(batch.map(processFn));
        results.push(...batchResults);
      }
      return results;
    }

    const BATCH_SIZE = 50; // 每批处理 50 个
    await processBatches(updates, BATCH_SIZE, update => prisma.chunks.update(update));

    // 记录操作日志（可选）
    console.log(`Successfully updated ${chunksToUpdate.length} chunks`);

    return NextResponse.json({
      success: true,
      updatedCount: chunksToUpdate.length,
      message: `Successfully updated ${chunksToUpdate.length} chunks`
    });
  } catch (error) {
    console.error('批量编辑文本块失败:', error);

    return NextResponse.json(
      {
        error: 'Batch edit chunks failed',
        details: error.message
      },
      { status: 500 }
    );
  } finally {
    await prisma.$disconnect();
  }
}

const http = require('http');
const path = require('path');
const fs = require('fs');
const { dialog } = require('electron');

/**
 * 检查端口是否被占用
 * @param {number} port 端口号
 * @returns {Promise<boolean>} 端口是否被占用
 */
function checkPort(port) {
  return new Promise(resolve => {
    const server = http.createServer();
    server.once('error', () => {
      resolve(true); // 端口被占用
    });
    server.once('listening', () => {
      server.close();
      resolve(false); // 端口未被占用
    });
    server.listen(port);
  });
}

/**
 * 启动 Next.js 服务
 * @param {number} port 端口号
 * @param {Object} app Electron app 对象
 * @returns {Promise<string>} 服务URL
 */
async function startNextServer(port, app) {
  console.log(`Easy Dataset 客户端启动中，当前版本: ${require('../util').getAppVersion()}`);

  // 设置日志文件路径
  const logDir = path.join(app.getPath('userData'), 'logs');
  if (!fs.existsSync(logDir)) {
    fs.mkdirSync(logDir, { recursive: true });
  }
  const logFile = path.join(logDir, `nextjs-${new Date().toISOString().replace(/:/g, '-')}.log`);
  const logStream = fs.createWriteStream(logFile, { flags: 'a' });

  // 重定向 console.log 和 console.error
  const originalConsoleLog = console.log;
  const originalConsoleError = console.error;

  console.log = function () {
    const args = Array.from(arguments);
    const logMessage = args.map(arg => (typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg)).join(' ');

    logStream.write(`[${new Date().toISOString()}] [LOG] ${logMessage}\n`);
    originalConsoleLog.apply(console, args);
  };

  console.error = function () {
    const args = Array.from(arguments);
    const logMessage = args.map(arg => (typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg)).join(' ');

    logStream.write(`[${new Date().toISOString()}] [ERROR] ${logMessage}\n`);
    originalConsoleError.apply(console, args);
  };

  // 检查端口是否被占用
  const isPortBusy = await checkPort(port);
  if (isPortBusy) {
    console.log(`端口 ${port} 已被占用，尝试直接连接...`);
    return `http://localhost:${port}`;
  }

  console.log(`启动 Next.js 服务，端口: ${port}`);

  try {
    // 动态导入 Next.js
    const next = require('next');
    const nextApp = next({
      dev: false,
      dir: path.join(__dirname, '../..'),
      conf: {
        // 配置 Next.js 的日志输出
        onInfo: info => {
          console.log(`[Next.js Info] ${info}`);
        },
        onError: error => {
          console.error(`[Next.js Error] ${error}`);
        },
        onWarn: warn => {
          console.log(`[Next.js Warning] ${warn}`);
        }
      }
    });
    const handle = nextApp.getRequestHandler();

    await nextApp.prepare();

    const server = http.createServer((req, res) => {
      // 记录请求日志
      console.log(`[Request] ${req.method} ${req.url}`);
      handle(req, res);
    });

    return new Promise(resolve => {
      server.listen(port, err => {
        if (err) throw err;
        console.log(`服务已启动，正在打开应用...`);
        resolve(`http://localhost:${port}`);
      });
    });
  } catch (error) {
    console.error('启动服务失败:', error);
    dialog.showErrorBox('启动失败', `无法启动 Next.js 服务: ${error.message}`);
    app.quit();
    return '';
  }
}

module.exports = {
  checkPort,
  startNextServer
};

import { getQuestionById, updateQuestion } from '@/lib/db/questions';
import {
  createDataset,
  deleteDataset,
  getDatasetsById,
  getDatasetsByPagination,
  getDatasetsIds,
  updateDataset
} from '@/lib/db/datasets';
import { getProject } from '@/lib/db/projects';
import getAnswerPrompt from '@/lib/llm/prompts/answer';
import getAnswerEnPrompt from '@/lib/llm/prompts/answerEn';
import getEnhancedAnswerPrompt from '@/lib/llm/prompts/enhancedAnswer';
import getEnhancedAnswerEnPrompt from '@/lib/llm/prompts/enhancedAnswerEn';
import getOptimizeCotPrompt from '@/lib/llm/prompts/optimizeCot';
import getOptimizeCotEnPrompt from '@/lib/llm/prompts/optimizeCotEn';
import { getChunkById } from '@/lib/db/chunks';
import { getActiveGaPairsByFileId } from '@/lib/db/ga-pairs';
import { nanoid } from 'nanoid';
import LLMClient from '@/lib/llm/core/index';
import logger from '@/lib/util/logger';

/**
 * 优化思维链
 * @param {string} originalQuestion - 原始问题
 * @param {string} answer - 答案
 * @param {string} originalCot - 原始思维链
 * @param {string} language - 语言
 * @param {object} llmClient - LLM客户端
 * @param {string} id - 数据集ID
 * @param {string} projectId - 项目ID
 */
async function optimizeCot(originalQuestion, answer, originalCot, language, llmClient, id, projectId) {
  try {
    const prompt =
      language === 'en'
        ? getOptimizeCotEnPrompt(originalQuestion, answer, originalCot)
        : getOptimizeCotPrompt(originalQuestion, answer, originalCot);
    const { answer: as, cot } = await llmClient.getResponseWithCOT(prompt);
    const optimizedAnswer = as || cot;
    const result = await updateDataset({ id, cot: optimizedAnswer.replace('优化后的思维链', '') });
    logger.info(`成功优化思维链: ${originalQuestion}, ID: ${id}`);
    return result;
  } catch (error) {
    logger.error(`优化思维链失败: ${error.message}`);
    throw error;
  }
}

/**
 * 为单个问题生成答案并创建数据集
 * @param {string} projectId - 项目ID
 * @param {string} questionId - 问题ID
 * @param {object} options - 选项
 * @param {string} options.model - 模型名称
 * @param {string} options.language - 语言(中文/en)
 * @returns {Promise<Object>} 生成的数据集
 */
export async function generateDatasetForQuestion(projectId, questionId, options) {
  try {
    const { model, language = '中文' } = options;

    // 验证参数
    if (!projectId || !questionId || !model) {
      throw new Error('缺少必要参数');
    }

    // 获取问题
    const question = await getQuestionById(questionId);
    if (!question) {
      throw new Error('问题不存在');
    }

    // 获取文本块内容
    const chunk = await getChunkById(question.chunkId);
    if (!chunk) {
      throw new Error('文本块不存在');
    }
    const idDistill = chunk.name === 'Distilled Content';

    // 获取项目配置
    const project = await getProject(projectId);
    const { globalPrompt, answerPrompt } = project;

    // 创建LLM客户端
    const llmClient = new LLMClient(model);
    let activeGaPairs = [];
    let questionLinkedGaPair = null;
    let useEnhancedPrompt = false;

    if (chunk.fileId && !idDistill) {
      try {
        activeGaPairs = await getActiveGaPairsByFileId(chunk.fileId);

        // 优先检查问题是否关联了特定的GA pair
        if (question.gaPairId) {
          questionLinkedGaPair = activeGaPairs.find(ga => ga.id === question.gaPairId);
          if (questionLinkedGaPair) {
            useEnhancedPrompt = true;
            logger.info(`问题关联GA pair: ${questionLinkedGaPair.genreTitle}+${questionLinkedGaPair.audienceTitle}`);
          }
        }

        logger.info(`检查到激活的GA对，${useEnhancedPrompt ? '使用' : '不使用'}增强提示词`);
      } catch (error) {
        logger.warn(`获取GA pairs失败，使用标准提示词: ${error.message}`);
        useEnhancedPrompt = false;
      }
    }

    let prompt;

    if (idDistill) {
      // 对于精炼内容，直接使用问题
      prompt = question.question;
    } else if (useEnhancedPrompt) {
      // 使用MGA增强提示词
      const enhancedPromptFunc = language === 'en' ? getEnhancedAnswerEnPrompt : getEnhancedAnswerPrompt;

      //使用问题关联的GA pair
      let primaryGaPair;

      primaryGaPair = {
        genre: `${questionLinkedGaPair.genreTitle}: ${questionLinkedGaPair.genreDesc}`,
        audience: `${questionLinkedGaPair.audienceTitle}: ${questionLinkedGaPair.audienceDesc}`,
        active: questionLinkedGaPair.isActive
      };
      logger.info(`使用问题关联的GA pair: ${primaryGaPair.genre} | ${primaryGaPair.audience}`);

      prompt = enhancedPromptFunc({
        text: chunk.content,
        question: question.question,
        globalPrompt,
        answerPrompt,
        activeGaPair: primaryGaPair
      });

      logger.info(`使用MGA增强提示词生成答案`);
    } else {
      // 使用标准提示词
      const promptFunc = language === 'en' ? getAnswerEnPrompt : getAnswerPrompt;
      prompt = promptFunc({
        text: chunk.content,
        question: question.question,
        globalPrompt,
        answerPrompt
      });

      logger.info('使用标准提示词生成答案');
    }

    // 调用大模型生成答案
    const { answer, cot } = await llmClient.getResponseWithCOT(prompt);

    const datasetId = nanoid(12);

    // 创建新的数据集项
    const datasets = {
      id: datasetId,
      projectId: projectId,
      question: question.question,
      answer: answer,
      model: model.modelName,
      cot: cot,
      questionLabel: question.label || null
    };

    let chunkData = await getChunkById(question.chunkId);
    datasets.chunkName = chunkData.name;
    datasets.chunkContent = ''; // 不再保存原始文本块内容
    datasets.questionId = question.id;

    let dataset = await createDataset(datasets);
    if (cot && !idDistill) {
      // 为了性能考虑，这里异步优化
      optimizeCot(question.question, answer, cot, language, llmClient, datasetId, projectId);
    }
    if (dataset) {
      await updateQuestion({ id: questionId, answered: true });
    }

    const logMessage = useEnhancedPrompt
      ? `成功生成MGA增强数据集: ${question.question}`
      : `成功生成标准数据集: ${question.question}`;
    logger.info(logMessage);

    return {
      success: true,
      dataset,
      mgaEnhanced: useEnhancedPrompt,
      activePairs: activeGaPairs.length
    };
  } catch (error) {
    logger.error(`生成数据集失败: ${error.message}`);
    throw error;
  }
}

export default {
  generateDatasetForQuestion,
  optimizeCot
};

'use server';
import { db } from '@/lib/db/index';
import fs from 'fs';
import path from 'path';
import { deleteDataset } from './datasets';
import { deleteChunkById } from './chunks';

//获取文件列表
export async function getUploadFilesPagination(projectId, page = 1, pageSize = 10, fileName) {
  try {
    const whereClause = {
      projectId,
      fileName: { contains: fileName }
    };
    const [data, total] = await Promise.all([
      db.uploadFiles.findMany({
        where: whereClause,
        orderBy: {
          createAt: 'desc'
        },
        skip: (page - 1) * pageSize,
        take: pageSize
      }),
      db.uploadFiles.count({
        where: whereClause
      })
    ]);
    return { data, total };
  } catch (error) {
    console.error('Failed to get uploadFiles by pagination in database');
    throw error;
  }
}

export async function getUploadFileInfoById(fileId) {
  try {
    return await db.uploadFiles.findUnique({ where: { id: fileId } });
  } catch (error) {
    console.error('Failed to get uploadFiles by id in database');
    throw error;
  }
}

export async function getUploadFilesByProjectId(projectId) {
  try {
    return await db.uploadFiles.findMany({
      where: {
        projectId,
        NOT: {
          id: {
            in: await db.chunks
              .findMany({
                where: { projectId },
                select: { fileId: true }
              })
              .then(chunks => chunks.map(chunk => chunk.fileId))
          }
        }
      }
    });
  } catch (error) {
    console.error('Failed to get uploadFiles by id in database');
    throw error;
  }
}

export async function checkUploadFileInfoByMD5(projectId, md5) {
  try {
    return await db.uploadFiles.findFirst({
      where: {
        projectId,
        md5
      }
    });
  } catch (error) {
    console.error('Failed to check uploadFiles by md5 in database');
    throw error;
  }
}

export async function createUploadFileInfo(fileInfo) {
  try {
    return await db.uploadFiles.create({ data: fileInfo });
  } catch (error) {
    console.error('Failed to get uploadFiles by id in database');
    throw error;
  }
}

export async function delUploadFileInfoById(fileId) {
  try {
    // 1. 获取文件信息
    let fileInfo = await db.uploadFiles.findUnique({ where: { id: fileId } });
    if (!fileInfo) {
      throw new Error('File not found');
    }

    // 2. 获取与文件关联的所有文本块
    const chunks = await db.chunks.findMany({
      where: { fileId: fileId }
    });

    // 记录统计数据，用于返回给前端显示
    const chunkIds = chunks.map(chunk => chunk.id);
    const stats = {
      chunks: chunks.length,
      questions: 0,
      datasets: 0
    };

    // 3. 找出所有关联的问题和数据集
    let questionIds = [];
    let datasets = [];

    if (chunkIds.length > 0) {
      // 统计问题数量
      const questionsCount = await db.questions.count({
        where: { chunkId: { in: chunkIds } }
      });
      stats.questions = questionsCount;

      // 获取所有问题ID
      const questions = await db.questions.findMany({
        where: { chunkId: { in: chunkIds } },
        select: { id: true }
      });

      questionIds = questions.map(q => q.id);

      // 4. 统计数据集数量
      if (questionIds.length > 0) {
        const datasetsCount = await db.datasets.count({
          where: { questionId: { in: questionIds } }
        });
        stats.datasets = datasetsCount;

        // 获取所有数据集
        datasets = await db.datasets.findMany({
          where: { questionId: { in: questionIds } },
          select: { id: true }
        });
      }
    }

    // 5. 使用事务批量删除所有数据库数据
    // 按照外键依赖关系从外到内删除
    const deleteOperations = [];

    // 先删除数据集
    if (datasets.length > 0) {
      deleteOperations.push(
        db.datasets.deleteMany({
          where: { id: { in: datasets.map(d => d.id) } }
        })
      );
    }

    // 再删除问题
    if (questionIds.length > 0) {
      deleteOperations.push(
        db.questions.deleteMany({
          where: { id: { in: questionIds } }
        })
      );
    }

    // 然后删除文本块
    if (chunkIds.length > 0) {
      deleteOperations.push(
        db.chunks.deleteMany({
          where: { id: { in: chunkIds } }
        })
      );
    }

    // 最后删除文件记录
    deleteOperations.push(
      db.uploadFiles.delete({
        where: { id: fileId }
      })
    );

    // 执行数据库事务，确保原子性
    await db.$transaction(deleteOperations);

    // 6. 删除文件系统中的文件
    let projectPath = path.join(fileInfo.path, fileInfo.fileName);
    if (fileInfo.fileExt !== '.md') {
      let filePath = path.join(fileInfo.path, fileInfo.fileName.replace(/\.[^/.]+$/, '.md'));
      if (fs.existsSync(filePath)) {
        await fs.promises.rm(filePath, { recursive: true });
      }
    }
    if (fs.existsSync(projectPath)) {
      await fs.promises.rm(projectPath, { recursive: true });
    }

    return { success: true, stats, fileName: fileInfo.fileName, fileInfo };
  } catch (error) {
    console.error('Failed to delete uploadFiles by id in database:', error);
    throw error;
  }
}

'use server';
import { db } from '@/lib/db/index';
import { ensureDir, getProjectRoot } from '@/lib/db/base';
import path from 'path';
import fs from 'fs';

export async function saveChunks(chunks) {
  try {
    return await db.chunks.createMany({ data: chunks });
  } catch (error) {
    console.error('Failed to create chunks in database');
    throw error;
  }
}

export async function getChunkById(chunkId) {
  try {
    return await db.chunks.findUnique({ where: { id: chunkId } });
  } catch (error) {
    console.error('Failed to get chunks by id in database');
    throw error;
  }
}

export async function getChunksByFileIds(fileIds) {
  try {
    return await db.chunks.findMany({
      where: { fileId: { in: fileIds } },
      include: {
        Questions: {
          select: {
            question: true
          }
        }
      }
    });
  } catch (error) {
    console.error('Failed to get chunks by id in database');
    throw error;
  }
}

// 获取项目中所有文本片段的ID
export async function getChunkByProjectId(projectId, filter) {
  try {
    const whereClause = {
      projectId,
      NOT: {
        name: {
          contains: 'Distilled Content'
        }
      }
    };
    if (filter === 'generated') {
      whereClause.Questions = {
        some: {}
      };
    } else if (filter === 'ungenerated') {
      whereClause.Questions = {
        none: {}
      };
    }
    return await db.chunks.findMany({
      where: whereClause,
      include: {
        Questions: {
          select: {
            question: true
          }
        }
      }
    });
  } catch (error) {
    console.error('Failed to get chunks by projectId in database');
    throw error;
  }
}

export async function deleteChunkById(chunkId) {
  try {
    const delQuestions = db.questions.deleteMany({ where: { chunkId } });
    const delChunk = db.chunks.delete({ where: { id: chunkId } });
    return await db.$transaction([delQuestions, delChunk]);
  } catch (error) {
    console.error('Failed to delete chunks by id in database');
    throw error;
  }
}

/**
 * 根据文本块名称获取文本块
 * @param {string} projectId - 项目ID
 * @param {string} chunkName - 文本块名称
 * @returns {Promise} - 查询结果
 */
export async function getChunkByName(projectId, chunkName) {
  try {
    return await db.chunks.findFirst({
      where: {
        projectId,
        name: chunkName
      }
    });
  } catch (error) {
    console.error('根据名称获取文本块失败', error);
    throw error;
  }
}

/**
 * 根据文件ID删除所有相关文本块
 * @param {string} projectId - 项目ID
 * @param {string} fileId - 文件ID
 * @returns {Promise} - 删除操作的结果
 */
export async function deleteChunksByFileId(projectId, fileId) {
  try {
    // 查找与该文件相关的所有文本块
    const chunks = await db.chunks.findMany({
      where: { projectId, fileId },
      select: { id: true }
    });

    // 提取文本块ID
    const chunkIds = chunks.map(chunk => chunk.id);

    // 如果没有找到文本块，直接返回
    if (chunkIds.length === 0) {
      return { count: 0 };
    }

    // 删除相关的问题
    const delQuestions = db.questions.deleteMany({
      where: { chunkId: { in: chunkIds } }
    });

    // 删除文本块
    const delChunks = db.chunks.deleteMany({
      where: { id: { in: chunkIds } }
    });

    // 使用事务确保原子性操作
    const result = await db.$transaction([delQuestions, delChunks]);

    return { count: result[1].count };
  } catch (error) {
    console.error('删除文件相关文本块失败:', error);
    throw error;
  }
}

export async function updateChunkById(chunkId, chunkData) {
  try {
    return await db.chunks.update({ where: { id: chunkId }, data: chunkData });
  } catch (error) {
    console.error('Failed to update chunks by id in database');
    throw error;
  }
}

// 删除文件及相关TOC文件
// TODO 后期优化 将文件也新增表结构关联 防止删除错误
export async function deleteChunkAndFile(projectId, fileName) {
  try {
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);
    const filesDir = path.join(projectPath, 'files');
    const tocDir = path.join(projectPath, 'toc');

    // 确保目录存在
    await ensureDir(tocDir);

    // 删除原始文件
    const filePath = path.join(filesDir, fileName);
    try {
      await fs.promises.access(filePath);
      await fs.promises.unlink(filePath);
    } catch (error) {
      console.error(`删除文件 ${fileName} 失败:`, error);
      // 如果文件不存在，继续处理
    }

    // 删除相关的TOC文件
    const baseName = path.basename(fileName, path.extname(fileName));
    const tocPath = path.join(filesDir, `${baseName}-toc.json`);
    try {
      await fs.promises.access(tocPath);
      await fs.promises.unlink(tocPath);
    } catch (error) {
      // 如果TOC文件不存在，继续处理
    }

    // TODO 暂不删除数据库中Chunk数据 如果删除 Question Dataset关联的Chunk数据是否也要删除？
    // return await db.chunks.deleteMany({
    //     where: {
    //         name: {
    //             startsWith: baseName + '-part-',
    //         }, projectId
    //     }
    // });
  } catch (error) {
    console.error('Failed to delete chunks by id in database');
    throw error;
  }
}

'use server';
import { db } from '@/lib/db/index';

/**
 * 获取文件的所有 GA 对
 * @param {string} fileId - 文件 ID
 * @returns {Promise<Array>} GA 对列表
 */
export async function getGaPairsByFileId(fileId) {
  try {
    return await db.gaPairs.findMany({
      where: { fileId },
      orderBy: { pairNumber: 'asc' }
    });
  } catch (error) {
    console.error('Failed to get GA pairs by fileId in database:', error);
    throw error;
  }
}

/**
 * 获取项目的所有 GA 对
 * @param {string} projectId - 项目 ID
 * @returns {Promise<Array>} GA 对列表
 */
export async function getGaPairsByProjectId(projectId) {
  try {
    return await db.gaPairs.findMany({
      where: { projectId },
      include: {
        uploadFile: {
          select: {
            fileName: true
          }
        }
      },
      orderBy: [{ fileId: 'asc' }, { pairNumber: 'asc' }]
    });
  } catch (error) {
    console.error('Failed to get GA pairs by projectId in database:', error);
    throw error;
  }
}

/**
 * 创建 GA 对
 * @param {Array} gaPairs - GA 对数据数组
 * @returns {Promise<Array>} 创建的 GA 对
 */
export async function createGaPairs(gaPairs) {
  try {
    return await db.gaPairs.createManyAndReturn({ data: gaPairs });
  } catch (error) {
    console.error('Failed to create GA pairs in database:', error);
    throw error;
  }
}

/**
 * 更新单个 GA 对
 * @param {string} id - GA 对 ID
 * @param {Object} data - 更新数据
 * @returns {Promise<Object>} 更新后的 GA 对
 */
export async function updateGaPair(id, data) {
  try {
    return await db.gaPairs.update({
      where: { id },
      data
    });
  } catch (error) {
    console.error('Failed to update GA pair in database:', error);
    throw error;
  }
}

/**
 * 删除文件的所有 GA 对
 * @param {string} fileId - 文件 ID
 * @returns {Promise<Object>} 删除结果
 */
export async function deleteGaPairsByFileId(fileId) {
  try {
    return await db.gaPairs.deleteMany({
      where: { fileId }
    });
  } catch (error) {
    console.error('Failed to delete GA pairs by fileId in database:', error);
    throw error;
  }
}

/**
 * 切换 GA 对的激活状态
 * @param {string} id - GA 对 ID
 * @param {boolean} isActive - 激活状态
 * @returns {Promise<Object>} 更新后的 GA 对
 */
export async function toggleGaPairActive(id, isActive) {
  try {
    return await db.gaPairs.update({
      where: { id },
      data: { isActive }
    });
  } catch (error) {
    console.error('Failed to toggle GA pair active status in database:', error);
    throw error;
  }
}

/**
 * 获取文件的激活 GA 对
 * @param {string} fileId - 文件 ID
 * @returns {Promise<Array>} 激活的 GA 对列表
 */
export async function getActiveGaPairsByFileId(fileId) {
  try {
    return await db.gaPairs.findMany({
      where: {
        fileId,
        isActive: true
      },
      orderBy: { pairNumber: 'asc' }
    });
  } catch (error) {
    console.error('Failed to get active GA pairs by fileId in database:', error);
    throw error;
  }
}

/**
 * 批量更新 GA 对
 * @param {Array} updates - 更新数据数组，每个包含 id 和其他字段
 * @returns {Promise<Array>} 更新结果
 */
export async function batchUpdateGaPairs(updates) {
  try {
    const results = await Promise.all(
      updates.map(({ id, ...updateData }) => {
        // 过滤掉不应该更新的字段
        const { createAt, updateAt, projectId, fileId, pairNumber, ...data } = updateData;

        // 确保有数据需要更新
        if (Object.keys(data).length === 0) {
          console.warn(`No data to update for GA pair ${id}`);
          return Promise.resolve(null);
        }

        return db.gaPairs.update({
          where: { id },
          data
        });
      })
    );

    // 过滤掉null结果
    return results.filter(result => result !== null);
  } catch (error) {
    console.error('Failed to batch update GA pairs in database:', error);
    throw error;
  }
}

/**
 * Generate and save GA pairs for a specific file using LLM
 * @param {string} projectId - Project ID
 * @param {string} fileId - File ID
 * @param {Array} gaPairs - Array of GA pair objects from LLM
 * @returns {Promise<Array>} - Created GA pairs
 */
export async function saveGaPairs(projectId, fileId, gaPairs) {
  try {
    // First, delete existing GA pairs for this file to avoid conflicts
    await db.gaPairs.deleteMany({
      where: { projectId, fileId }
    });

    // Map the GA pairs to database format
    const gaPairData = gaPairs.map((pair, index) => ({
      projectId,
      fileId,
      pairNumber: index + 1, // 1-5
      genreTitle: pair.genre.title,
      genreDesc: pair.genre.description,
      audienceTitle: pair.audience.title,
      audienceDesc: pair.audience.description,
      isActive: true
    }));

    return await db.gaPairs.createMany({ data: gaPairData });
  } catch (error) {
    console.error('Failed to save GA pairs in database:', error);
    throw error;
  }
}

/**
 * Check if GA pairs exist for a file
 * @param {string} projectId - Project ID
 * @param {string} fileId - File ID
 * @returns {Promise<boolean>} - Whether GA pairs exist
 */
export async function hasGaPairs(projectId, fileId) {
  try {
    const count = await db.gaPairs.count({
      where: { projectId, fileId }
    });
    return count > 0;
  } catch (error) {
    console.error('Failed to check GA pairs existence:', error);
    throw error;
  }
}

/**
 * Get GA pair statistics for a project
 * @param {string} projectId - Project ID
 * @returns {Promise<Object>} - GA pair statistics
 */
export async function getGaPairStats(projectId) {
  try {
    const totalCount = await db.gaPairs.count({
      where: { projectId }
    });

    const activeCount = await db.gaPairs.count({
      where: { projectId, isActive: true }
    });

    const filesWithGaPairs = await db.gaPairs.groupBy({
      by: ['fileId'],
      where: { projectId }
    });

    return {
      totalPairs: totalCount,
      activePairs: activeCount,
      filesWithPairs: filesWithGaPairs.length
    };
  } catch (error) {
    console.error('Failed to get GA pair statistics:', error);
    throw error;
  }
}

'use client';

import React, { useState, useEffect, useRef } from 'react';
import {
  Box,
  Button,
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  CircularProgress,
  Typography,
  Divider,
  Chip,
  IconButton,
  Switch,
  FormControlLabel,
  Tooltip,
  Alert,
  DialogContentText
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add';
import SaveIcon from '@mui/icons-material/Save';
import ReactMarkdown from 'react-markdown';
import { useTranslation } from 'react-i18next';

export default function MarkdownViewDialog({ open, text, onClose, projectId, onSaveSuccess }) {
  const { t } = useTranslation();
  const [customSplitMode, setCustomSplitMode] = useState(false);
  const [splitPoints, setSplitPoints] = useState([]);
  const [selectedText, setSelectedText] = useState('');
  const [savedMessage, setSavedMessage] = useState('');
  const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState('');
  const contentRef = useRef(null);
  const [chunksPreview, setChunksPreview] = useState([]);

  // 根据分块点计算每个块的字数
  const calculateChunksPreview = points => {
    if (!text || !text.content) return [];

    const content = text.content;
    const sortedPoints = [...points].sort((a, b) => a.position - b.position);

    const chunks = [];
    let startPos = 0;

    // 计算每个分块
    for (let i = 0; i < sortedPoints.length; i++) {
      const endPos = sortedPoints[i].position;
      const chunkContent = content.substring(startPos, endPos);

      if (chunkContent.trim().length > 0) {
        chunks.push({
          index: i + 1,
          length: chunkContent.length,
          preview: chunkContent.substring(0, 20) + (chunkContent.length > 20 ? '...' : '')
        });
      }

      startPos = endPos;
    }

    // 添加最后一个分块
    const lastChunkContent = content.substring(startPos);
    if (lastChunkContent.trim().length > 0) {
      chunks.push({
        index: chunks.length + 1,
        length: lastChunkContent.length,
        preview: lastChunkContent.substring(0, 20) + (lastChunkContent.length > 20 ? '...' : '')
      });
    }

    return chunks;
  };

  // 重置组件状态
  useEffect(() => {
    if (!open) {
      setSplitPoints([]);
      setCustomSplitMode(false);
      setSelectedText('');
      setSavedMessage('');
    }
  }, [open]);

  // 当分块点变化时更新预览
  useEffect(() => {
    if (splitPoints.length > 0 && text?.content) {
      const preview = calculateChunksPreview(splitPoints);
      setChunksPreview(preview);
    } else {
      setChunksPreview([]);
    }
  }, [splitPoints, text?.content]);

  // 处理用户选择文本事件
  const handleTextSelection = () => {
    if (!customSplitMode) return;

    const selection = window.getSelection();
    if (!selection.toString().trim()) return;

    // 获取选择的文本内容和位置
    const selectedContent = selection.toString();

    // 计算选择位置在文档中的偏移量
    const range = selection.getRangeAt(0);
    const preCaretRange = range.cloneRange();
    preCaretRange.selectNodeContents(contentRef.current);
    preCaretRange.setEnd(range.endContainer, range.endOffset);
    const position = preCaretRange.toString().length;

    // 添加到分割点列表
    const newPoint = {
      id: Date.now(),
      position,
      preview: selectedContent.substring(0, 40) + (selectedContent.length > 40 ? '...' : '')
    };

    setSplitPoints(prev => [...prev, newPoint].sort((a, b) => a.position - b.position));
    setSelectedText('');
  };

  // 删除分割点
  const handleDeletePoint = id => {
    setSplitPoints(prev => prev.filter(point => point.id !== id));
  };

  // 弹出确认对话框
  const handleConfirmSave = () => {
    setConfirmDialogOpen(true);
  };

  // 取消保存
  const handleCancelSave = () => {
    setConfirmDialogOpen(false);
  };

  // 确认并执行保存
  const handleSavePoints = async () => {
    // 输出调试信息
    console.log('保存分块点时的数据:', {
      projectId,
      text: text
        ? {
            fileId: text.fileId,
            fileName: text.fileName,
            contentLength: text.content ? text.content.length : 0
          }
        : null,
      splitPointsCount: splitPoints.length
    });

    if (!text) {
      setError(t('textSplit.missingRequiredData') + ': text 为空');
      return;
    }

    if (!text.fileId) {
      setError(t('textSplit.missingRequiredData') + ': fileId 不存在');
      return;
    }

    if (!text.fileName) {
      setError(t('textSplit.missingRequiredData') + ': fileName 不存在');
      return;
    }

    if (!text.content) {
      setError(t('textSplit.missingRequiredData') + ': content 不存在');
      return;
    }

    if (!projectId) {
      setError(t('textSplit.missingRequiredData') + ': projectId 不存在');
      return;
    }

    setConfirmDialogOpen(false);
    setSaving(true);
    setError('');

    try {
      // 准备要发送的数据
      const customSplitData = {
        fileId: text.fileId,
        fileName: text.fileName,
        content: text.content,
        splitPoints: splitPoints.map(point => ({
          position: point.position,
          preview: point.preview
        }))
      };

      // 发送请求到待创建的API接口
      const response = await fetch(`/api/projects/${projectId}/custom-split`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(customSplitData)
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.error || t('textSplit.customSplitFailed'));
      }

      // 保存成功
      setSavedMessage(t('textSplit.customSplitSuccess'));

      // 短暂显示成功消息后关闭对话框并刷新列表
      setTimeout(() => {
        setSavedMessage('');

        // 关闭对话框
        onClose();

        // 调用父组件的刷新方法（如果提供了）
        if (typeof onSaveSuccess === 'function') {
          onSaveSuccess();
        }
      }, 1500);
    } catch (err) {
      console.error('保存自定义分块出错:', err);
      setError(err.message || t('textSplit.customSplitFailed'));
    } finally {
      setSaving(false);
    }
  };

  return (
    <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
      <DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <Typography variant="h6">{text ? text.fileName : ''}</Typography>
        <FormControlLabel
          control={
            <Switch checked={customSplitMode} onChange={e => setCustomSplitMode(e.target.checked)} color="primary" />
          }
          label={t('textSplit.customSplitMode')}
          sx={{ ml: 2 }}
        />
      </DialogTitle>

      {customSplitMode && (
        <Box sx={{ px: 3, py: 1, bgcolor: 'action.hover' }}>
          <Typography variant="subtitle2" color="textSecondary" gutterBottom>
            {t('textSplit.customSplitInstructions')}
          </Typography>

          {/* 分割点列表 */}
          {splitPoints.length > 0 && (
            <Box sx={{ mt: 1, mb: 2 }}>
              <Typography variant="body2" sx={{ mb: 1 }}>
                {t('textSplit.splitPointsList')} ({splitPoints.length}):
              </Typography>
              <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
                {splitPoints.map((point, index) => (
                  <Chip
                    key={point.id}
                    label={`${index + 1}. ${point.preview}`}
                    onDelete={() => handleDeletePoint(point.id)}
                    deleteIcon={<DeleteIcon />}
                    color="primary"
                    variant="outlined"
                  />
                ))}
              </Box>

              {/* 文本块字数预览 */}
              {chunksPreview.length > 0 && (
                <Box
                  sx={{
                    mt: 2,
                    p: 1,
                    bgcolor: 'background.paper',
                    borderRadius: 1,
                    border: '1px dashed',
                    borderColor: 'divider'
                  }}
                >
                  <Typography variant="subtitle2" gutterBottom>
                    {t('textSplit.chunksPreview')}
                  </Typography>
                  <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
                    {chunksPreview.map(chunk => (
                      <Chip
                        key={chunk.index}
                        size="small"
                        label={`${t('textSplit.chunk')} ${chunk.index}: ${chunk.length}${t('textSplit.characters')}`}
                        color="info"
                        variant="outlined"
                      />
                    ))}
                  </Box>
                </Box>
              )}
            </Box>
          )}

          {/* 保存按钮 */}
          <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
            <Button
              variant="contained"
              startIcon={<SaveIcon />}
              disabled={splitPoints.length === 0 || saving}
              onClick={handleConfirmSave}
              size="small"
            >
              {saving ? t('common.saving') : t('textSplit.saveSplitPoints')}
            </Button>
          </Box>

          {/* 提示消息 */}
          {savedMessage && (
            <Alert severity="success" sx={{ mt: 1 }}>
              {savedMessage}
            </Alert>
          )}

          {error && (
            <Alert severity="error" sx={{ mt: 1 }}>
              {error}
            </Alert>
          )}
        </Box>
      )}

      <Divider />

      <DialogContent dividers>
        {text ? (
          <Box
            sx={{
              maxHeight: '60vh',
              overflow: 'auto',
              cursor: customSplitMode ? 'text' : 'default',
              position: 'relative',
              '::selection': {
                backgroundColor: customSplitMode ? 'primary.light' : 'inherit',
                color: customSplitMode ? 'primary.contrastText' : 'inherit'
              }
            }}
            onMouseUp={handleTextSelection}
            ref={contentRef}
          >
            {/* 渲染带有分割点标记的内容 */}
            {customSplitMode && splitPoints.length > 0 ? (
              <Box>
                <pre style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word', fontFamily: 'inherit' }}>
                  {text.content.split('').map((char, index) => {
                    const isSplitPoint = splitPoints.some(point => point.position === index);
                    const splitPointIndex = splitPoints.findIndex(point => point.position === index);

                    if (isSplitPoint) {
                      return (
                        <React.Fragment key={index}>
                          <span
                            style={{
                              display: 'inline-block',
                              width: '100%',
                              borderTop: '2px dashed #1976d2',
                              marginTop: '8px',
                              marginBottom: '8px',
                              position: 'relative'
                            }}
                          >
                            <span
                              style={{
                                position: 'absolute',
                                left: '0',
                                top: '-15px',
                                backgroundColor: '#1976d2',
                                color: 'white',
                                padding: '0 6px',
                                borderRadius: '4px',
                                fontSize: '12px'
                              }}
                            >
                              {splitPointIndex + 1}
                            </span>
                          </span>
                          {char}
                        </React.Fragment>
                      );
                    }
                    return char;
                  })}
                </pre>
              </Box>
            ) : (
              <Box>
                <ReactMarkdown>{text.content}</ReactMarkdown>
              </Box>
            )}
          </Box>
        ) : (
          <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
            <CircularProgress />
          </Box>
        )}
      </DialogContent>

      <DialogActions>
        <Button onClick={onClose}>{t('common.close')}</Button>
      </DialogActions>

      {/* 确认对话框 */}
      <Dialog
        open={confirmDialogOpen}
        onClose={handleCancelSave}
        aria-labelledby="alert-dialog-title"
        aria-describedby="alert-dialog-description"
      >
        <DialogTitle id="alert-dialog-title">{t('textSplit.confirmCustomSplitTitle')}</DialogTitle>
        <DialogContent>
          <DialogContentText id="alert-dialog-description">
            {t('textSplit.confirmCustomSplitMessage')}
          </DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button onClick={handleCancelSave}>{t('common.cancel')}</Button>
          <Button onClick={handleSavePoints} color="primary" variant="contained" autoFocus>
            {t('common.confirm')}
          </Button>
        </DialogActions>
      </Dialog>
    </Dialog>
  );
}

'use client';

import { useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';

/**
 * 文本块管理的自定义Hook
 * @param {string} projectId - 项目ID
 * @param {string} [currentFilter='all'] - 当前筛选条件
 * @returns {Object} - 文本块状态和操作方法
 */
export default function useChunks(projectId, currentFilter = 'all') {
  const { t } = useTranslation();
  const [chunks, setChunks] = useState([]);
  const [tocData, setTocData] = useState('');
  const [loading, setLoading] = useState(false);

  /**
   * 获取文本块列表
   * @param {string} filter - 筛选条件
   */
  const fetchChunks = useCallback(
    async (filter = 'all') => {
      try {
        setLoading(true);
        const response = await fetch(`/api/projects/${projectId}/split?filter=${filter}`);

        if (!response.ok) {
          const errorData = await response.json();
          throw new Error(errorData.error || t('textSplit.fetchChunksFailed'));
        }

        const data = await response.json();
        setChunks(data.chunks || []);

        // 如果有文件结果，处理详细信息
        if (data.toc) {
          console.log(t('textSplit.fileResultReceived'), data.fileResult);
          // 如果有目录结构，设置目录数据
          setTocData(data.toc);
        }
      } catch (error) {
        toast.error(error.message);
      } finally {
        setLoading(false);
      }
    },
    [projectId, t, setLoading, setChunks, setTocData]
  );

  /**
   * 处理删除文本块
   * @param {string} chunkId - 文本块ID
   */
  const handleDeleteChunk = useCallback(
    async chunkId => {
      try {
        const response = await fetch(`/api/projects/${projectId}/chunks/${encodeURIComponent(chunkId)}`, {
          method: 'DELETE'
        });

        if (!response.ok) {
          const errorData = await response.json();
          throw new Error(errorData.error || t('textSplit.deleteChunkFailed'));
        }

        // 更新文本块列表
        setChunks(prev => prev.filter(chunk => chunk.id !== chunkId));
      } catch (error) {
        toast.error(error.message);
      }
    },
    [projectId, t]
  );

  /**
   * 处理文本块编辑
   * @param {string} chunkId - 文本块ID
   * @param {string} newContent - 新内容
   */
  const handleEditChunk = useCallback(
    async (chunkId, newContent) => {
      try {
        setLoading(true);

        const response = await fetch(`/api/projects/${projectId}/chunks/${encodeURIComponent(chunkId)}`, {
          method: 'PATCH',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ content: newContent })
        });

        if (!response.ok) {
          const errorData = await response.json();
          throw new Error(errorData.error || t('textSplit.editChunkFailed'));
        }

        // 更新成功后使用当前筛选条件刷新文本块列表
        // 直接从 URL 获取当前筛选参数，确保获取到的是最新的值
        const url = new URL(window.location.href);
        const filterParam = url.searchParams.get('filter') || 'all';
        await fetchChunks(filterParam);

        toast.success(t('textSplit.editChunkSuccess'));
      } catch (error) {
        toast.error(error.message);
      } finally {
        setLoading(false);
      }
    },
    [projectId, t, fetchChunks]
  );

  /**
   * 设置文本块列表
   * @param {Array} data - 新的文本块列表
   */
  const updateChunks = useCallback(data => {
    setChunks(data);
  }, []);

  /**
   * 添加新的文本块
   * @param {Array} newChunks - 新的文本块列表
   */
  const addChunks = useCallback(newChunks => {
    setChunks(prev => {
      const updatedChunks = [...prev];
      newChunks.forEach(chunk => {
        if (!updatedChunks.find(c => c.id === chunk.id)) {
          updatedChunks.push(chunk);
        }
      });
      return updatedChunks;
    });
  }, []);

  /**
   * 设置TOC数据
   * @param {string} toc - TOC数据
   */
  const updateTocData = useCallback(toc => {
    if (toc) {
      setTocData(toc);
    }
  }, []);

  return {
    chunks,
    tocData,
    loading,
    fetchChunks,
    handleDeleteChunk,
    handleEditChunk,
    updateChunks,
    addChunks,
    updateTocData,
    setLoading
  };
}

'use client';

import React, { useState, useEffect } from 'react';
import { Box, Typography, Container, IconButton } from '@mui/material';
import { useTranslation } from 'react-i18next';
import axios from 'axios';
import TaskIcon from '@mui/icons-material/Task';
import { toast } from 'sonner';

// 导入任务管理组件
import TaskFilters from '@/components/tasks/TaskFilters';
import TasksTable from '@/components/tasks/TasksTable';

/**
 * 任务管理页面
 * 支持任务列表查看、筛选、中断和删除操作
 * 分页展示、国际化支持
 */

// 任务状态映射
const TASK_STATUS = {
  0: { label: '处理中', color: 'warning' },
  1: { label: '已完成', color: 'success' },
  2: { label: '失败', color: 'error' },
  3: { label: '已中断', color: 'default' }
};

// 任务类型映射
const TASK_TYPES = {
  'text-processing': '文献处理',
  'question-generation': '问题生成',
  'answer-generation': '答案生成',
  'data-distillation': '数据蒸馏',
  'pdf-processing': 'PDF解析'
};

export default function TasksPage({ params }) {
  const { projectId } = params;
  const { t } = useTranslation();

  // 状态管理
  const [loading, setLoading] = useState(false);
  const [tasks, setTasks] = useState([]);
  const [statusFilter, setStatusFilter] = useState('all');
  const [typeFilter, setTypeFilter] = useState('all');

  // 分页相关状态
  const [page, setPage] = useState(0);
  const [rowsPerPage, setRowsPerPage] = useState(10);
  const [totalCount, setTotalCount] = useState(0);

  // 获取任务列表
  const fetchTasks = async () => {
    if (!projectId) return;

    try {
      setLoading(true);
      // 构建查询参数
      let url = `/api/projects/${projectId}/tasks/list`;
      const queryParams = [];

      if (statusFilter !== 'all') {
        queryParams.push(`status=${statusFilter}`);
      }

      if (typeFilter !== 'all') {
        queryParams.push(`taskType=${typeFilter}`);
      }

      // 添加分页参数
      queryParams.push(`page=${page}`);
      queryParams.push(`limit=${rowsPerPage}`);

      if (queryParams.length > 0) {
        url += '?' + queryParams.join('&');
      }

      const response = await axios.get(url);
      if (response.data?.code === 0) {
        setTasks(response.data.data || []);
        // 设置总记录数
        setTotalCount(response.data.total || response.data.data?.length || 0);
      }
    } catch (error) {
      console.error('获取任务列表失败:', error);
      toast.error(t('tasks.fetchFailed'));
    } finally {
      setLoading(false);
    }
  };

  // 初始化和过滤器变更时获取任务列表
  useEffect(() => {
    fetchTasks();

    // 定时刷新处理中的任务
    const intervalId = setInterval(() => {
      if (statusFilter === 'all' || statusFilter === '0') {
        fetchTasks();
      }
    }, 5000); // 每5秒更新一次处理中的任务

    return () => clearInterval(intervalId);
  }, [projectId, statusFilter, typeFilter, page, rowsPerPage]);

  // 删除任务
  const handleDeleteTask = async taskId => {
    if (!confirm(t('tasks.confirmDelete'))) return;

    try {
      const response = await axios.delete(`/api/projects/${projectId}/tasks/${taskId}`);
      if (response.data?.code === 0) {
        toast.success(t('tasks.deleteSuccess'));
        fetchTasks();
      } else {
        toast.error(t('tasks.deleteFailed'));
      }
    } catch (error) {
      console.error('删除任务失败:', error);
      toast.error(t('tasks.deleteFailed'));
    }
  };

  // 中断任务
  const handleAbortTask = async taskId => {
    if (!confirm(t('tasks.confirmAbort'))) return;

    try {
      const response = await axios.patch(`/api/projects/${projectId}/tasks/${taskId}`, {
        status: 3, // 3 表示已中断
        detail: t('tasks.status.aborted'),
        note: t('tasks.status.aborted')
      });

      if (response.data?.code === 0) {
        toast.success(t('tasks.abortSuccess'));
        fetchTasks();
      } else {
        toast.error(t('tasks.abortFailed'));
      }
    } catch (error) {
      console.error('中断任务失败:', error);
      toast.error(t('tasks.abortFailed'));
    }
  };

  // 分页参数更改处理
  const handleChangePage = (event, newPage) => {
    setPage(newPage);
  };

  const handleChangeRowsPerPage = event => {
    setRowsPerPage(parseInt(event.target.value, 10));
    setPage(0);
  };

  return (
    <Container maxWidth="xl" sx={{ mt: 2, mb: 4 }}>
      <Box
        sx={{
          display: 'flex',
          justifyContent: 'space-between',
          alignItems: 'center',
          mb: 3,
          pl: 2,
          pr: 2
        }}
      >
        <Typography variant="h5" component="h1" gutterBottom>
          <TaskIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
          {t('tasks.title')}
        </Typography>

        {/* 任务筛选器组件 */}
        <TaskFilters
          statusFilter={statusFilter}
          setStatusFilter={setStatusFilter}
          typeFilter={typeFilter}
          setTypeFilter={setTypeFilter}
          loading={loading}
          onRefresh={fetchTasks}
        />
      </Box>

      {/* 任务表格组件 */}
      <TasksTable
        tasks={tasks}
        loading={loading}
        handleAbortTask={handleAbortTask}
        handleDeleteTask={handleDeleteTask}
        page={page}
        rowsPerPage={rowsPerPage}
        handleChangePage={handleChangePage}
        handleChangeRowsPerPage={handleChangeRowsPerPage}
        totalCount={totalCount}
      />
    </Container>
  );
}

import { generateText, streamText } from 'ai';

class BaseClient {
  constructor(config) {
    this.endpoint = config.endpoint || '';
    this.apiKey = config.apiKey || '';
    this.model = config.model || '';
    this.modelConfig = {
      temperature: config.temperature || 0.7,
      top_p: config.top_p || 0.9,
      max_tokens: config.max_tokens || 8192
    };
  }

  /**
   * chat（普通输出）
   */
  async chat(messages, options) {
    const lastMessage = messages[messages.length - 1];
    const prompt = lastMessage.content;
    const model = this._getModel();
    const result = await generateText({
      model,
      messages: this._convertJson(messages),
      temperature: options.temperature || this.modelConfig.temperature,
      topP: options.top_p || this.modelConfig.top_p,
      maxTokens: options.max_tokens || this.modelConfig.max_tokens
    });
    return result;
  }

  /**
   * chat（流式输出）
   */
  async chatStream(messages, options) {
    const lastMessage = messages[messages.length - 1];
    const prompt = lastMessage.content;
    const model = this._getModel();
    const stream = streamText({
      model,
      messages: this._convertJson(messages),
      temperature: options.temperature || this.modelConfig.temperature,
      topP: options.top_p || this.modelConfig.top_p,
      maxTokens: options.max_tokens || this.modelConfig.max_tokens
    });
    return stream.toTextStreamResponse();
  }

  // 抽象方法
  _getModel() {
    throw new Error('_getModel 子类方法必须实现');
  }

  /**
   * chat（纯API流式输出）
   */
  async chatStreamAPI(messages, options) {
    const model = this._getModel();
    const modelName = typeof model === 'function' ? model.modelName : this.model;

    // 构建请求数据
    const payload = {
      model: modelName,
      messages: this._convertJson(messages),
      temperature: options.temperature || this.modelConfig.temperature,
      top_p: options.top_p || this.modelConfig.top_p,
      max_tokens: options.max_tokens || this.modelConfig.max_tokens,
      stream: true // 开启流式输出
    };

    // 添加思维链相关参数
    payload.send_reasoning = true;
    payload.reasoning = true;

    try {
      // 发起流式请求
      const response = await fetch(
        `${this.endpoint.endsWith('/') ? this.endpoint : `${this.endpoint}/`}chat/completions`,
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${this.apiKey}`
          },
          body: JSON.stringify(payload)
        }
      );

      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`API请求失败: ${response.status} ${response.statusText}\n${errorText}`);
      }

      if (!response.body) {
        throw new Error('响应中没有可读取的数据流');
      }

      // 处理原始数据流，实现思维链的流式输出
      const reader = response.body.getReader();
      const encoder = new TextEncoder();
      const decoder = new TextDecoder();

      // 创建一个新的可读流
      const newStream = new ReadableStream({
        async start(controller) {
          let buffer = '';
          let isThinking = false; // 当前是否在输出思维链模式
          let pendingReasoning = null; // 等待输出的思维链

          // 输出文本内容
          const sendContent = text => {
            if (!text) return;

            // 如果正在输出思维链，需要先关闭思维链标签
            if (isThinking) {
              controller.enqueue(encoder.encode('</think>'));
              isThinking = false;
            }

            controller.enqueue(encoder.encode(text));
          };

          // 流式输出思维链
          const sendReasoning = text => {
            if (!text) return;

            // 如果还没有开始思维链输出，需要先添加思维链标签
            if (!isThinking) {
              controller.enqueue(encoder.encode('<think>'));
              isThinking = true;
            }

            controller.enqueue(encoder.encode(text));
          };

          try {
            while (true) {
              const { done, value } = await reader.read();

              if (done) {
                // 流结束时，如果还在思维链模式，关闭标签
                if (isThinking) {
                  controller.enqueue(encoder.encode('</think>'));
                }
                controller.close();
                break;
              }

              // 解析数据块
              const chunk = decoder.decode(value, { stream: true });
              buffer += chunk;

              // 处理数据行
              let boundary = buffer.indexOf('\n');
              while (boundary !== -1) {
                const line = buffer.substring(0, boundary).trim();
                buffer = buffer.substring(boundary + 1);

                if (line.startsWith('data:') && !line.includes('[DONE]')) {
                  try {
                    // 解析JSON数据
                    const jsonData = JSON.parse(line.substring(5).trim());
                    const deltaContent = jsonData.choices?.[0]?.delta?.content;
                    const deltaReasoning = jsonData.choices?.[0]?.delta?.reasoning_content;

                    // 如果有思维链内容，则实时流式输出
                    if (deltaReasoning) {
                      sendReasoning(deltaReasoning);
                    }

                    // 如果有正文内容也实时输出
                    if (deltaContent !== undefined && deltaContent !== null) {
                      sendContent(deltaContent);
                    }
                  } catch (e) {
                    // 忽略 JSON 解析错误
                    console.error('解析响应数据出错:', e);
                  }
                } else if (line.includes('[DONE]')) {
                  // 数据流结束，如果还在思维链模式，需要关闭思维链标签
                  if (isThinking) {
                    controller.enqueue(encoder.encode('</think>'));
                    isThinking = false;
                  }
                }

                boundary = buffer.indexOf('\n');
              }
            }
          } catch (error) {
            console.error('处理数据流时出错:', error);
            // 如果出错时正在输出思维链，要关闭思维链标签
            if (isThinking) {
              try {
                controller.enqueue(encoder.encode('</think>'));
              } catch (e) {
                console.error('关闭思维链标签出错:', e);
              }
            }
            controller.error(error);
          }
        }
      });

      // 最终返回响应流
      return new Response(newStream, {
        headers: {
          'Content-Type': 'text/plain', // 纯文本格式
          'Cache-Control': 'no-cache',
          Connection: 'keep-alive'
        }
      });
    } catch (error) {
      console.error('流式API调用出错:', error);
      throw error;
    }
  }

  _convertJson(data) {
    return data.map(item => {
      // 只处理 role 为 "user" 的项
      if (item.role !== 'user') return item;

      const newItem = {
        role: 'user',
        content: '',
        experimental_attachments: [],
        parts: []
      };

      // 情况1：content 是字符串
      if (typeof item.content === 'string') {
        newItem.content = item.content;
        newItem.parts.push({
          type: 'text',
          text: item.content
        });
      }
      // 情况2：content 是数组
      else if (Array.isArray(item.content)) {
        item.content.forEach(contentItem => {
          if (contentItem.type === 'text') {
            // 文本内容
            newItem.content = contentItem.text;
            newItem.parts.push({
              type: 'text',
              text: contentItem.text
            });
          } else if (contentItem.type === 'image_url') {
            // 图片内容
            const imageUrl = contentItem.image_url.url;

            // 提取文件名（如果没有则使用默认名）
            let fileName = 'image.jpg';
            if (imageUrl.startsWith('data:')) {
              // 如果是 base64 数据，尝试从 content type 获取扩展名
              const match = imageUrl.match(/^data:image\/(\w+);base64/);
              if (match) {
                fileName = `image.${match[1]}`;
              }
            }

            newItem.experimental_attachments.push({
              url: imageUrl,
              name: fileName,
              contentType: imageUrl.startsWith('data:') ? imageUrl.split(';')[0].replace('data:', '') : 'image/jpeg' // 默认为 jpeg
            });
          }
        });
      }

      return newItem;
    });
  }
}

module.exports = BaseClient;

'use client';

import { Box, Typography, Checkbox, Button, Select, MenuItem, Tooltip, Menu, IconButton } from '@mui/material';
import QuizIcon from '@mui/icons-material/Quiz';
import DownloadIcon from '@mui/icons-material/Download';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import EditIcon from '@mui/icons-material/Edit';
import axios from 'axios';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';

export default function ChunkListHeader({
  projectId,
  totalChunks,
  selectedChunks,
  onSelectAll,
  onBatchGenerateQuestions,
  onBatchEditChunks,
  questionFilter,
  setQuestionFilter,
  chunks = [], // 添加chunks参数，用于导出文本块
  selectedModel = {}
}) {
  const { t, i18n } = useTranslation();

  // 添加更多菜单的状态和锚点
  const [moreMenuAnchorEl, setMoreMenuAnchorEl] = useState(null);
  const isMoreMenuOpen = Boolean(moreMenuAnchorEl);

  // 打开更多菜单
  const handleMoreMenuClick = event => {
    setMoreMenuAnchorEl(event.currentTarget);
  };

  // 关闭更多菜单
  const handleMoreMenuClose = () => {
    setMoreMenuAnchorEl(null);
  };

  // 处理批量编辑，关闭菜单并调用原有函数
  const handleBatchEdit = () => {
    handleMoreMenuClose();
    onBatchEditChunks();
  };

  // 处理导出文本块，关闭菜单并调用原有函数
  const handleExport = () => {
    handleMoreMenuClose();
    handleExportChunks();
  };

  // 创建自动提取问题任务
  const handleCreateAutoQuestionTask = async () => {
    if (!projectId || !selectedModel?.id) {
      toast.error(t('textSplit.selectModelFirst', { defaultValue: '请先选择模型' }));
      return;
    }

    try {
      // 调用创建任务接口
      const response = await axios.post(`/api/projects/${projectId}/tasks`, {
        taskType: 'question-generation',
        modelInfo: selectedModel,
        language: i18n.language,
        detail: '批量生成问题任务'
      });

      if (response.data?.code === 0) {
        toast.success(t('tasks.createSuccess', { defaultValue: '后台任务已创建，系统将自动处理未生成问题的文本块' }));
      } else {
        toast.error(t('tasks.createFailed', { defaultValue: '创建任务失败' }) + ': ' + response.data?.message);
      }
    } catch (error) {
      console.error('创建自动提取问题任务失败:', error);
      toast.error(t('tasks.createFailed', { defaultValue: '创建任务失败' }) + ': ' + error.message);
    }
  };

  // 导出文本块为JSON文件的函数
  const handleExportChunks = () => {
    if (!chunks || chunks.length === 0) return;

    // 创建要导出的数据对象
    const exportData = chunks.map(chunk => ({
      name: chunk.name,
      projectId: chunk.projectId,
      fileName: chunk.fileName,
      content: chunk.content,
      summary: chunk.summary,
      size: chunk.size
    }));

    // 将数据转换为JSON字符串
    const jsonString = JSON.stringify(exportData, null, 2);

    // 创建Blob对象
    const blob = new Blob([jsonString], { type: 'application/json' });

    // 创建下载链接
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `text-chunks-export-${new Date().toISOString().split('T')[0]}.json`;

    // 触发下载
    document.body.appendChild(a);
    a.click();

    // 清理
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  };

  return (
    <Box
      sx={{
        display: 'flex',
        flexDirection: { xs: 'column', md: 'row' },
        justifyContent: 'space-between',
        alignItems: { xs: 'flex-start', md: 'center' },
        gap: 2,
        mb: 3
      }}
    >
      <Box sx={{ display: 'flex', alignItems: 'center' }}>
        <Checkbox
          checked={selectedChunks.length === totalChunks}
          indeterminate={selectedChunks.length > 0 && selectedChunks.length < totalChunks}
          onChange={onSelectAll}
        />
        <Typography variant="body1">
          {t('textSplit.selectedCount', { count: selectedChunks.length })} ,
          {t('textSplit.totalCount', { count: totalChunks })}
        </Typography>
      </Box>

      <Box
        sx={{
          display: 'flex',
          flexDirection: { xs: 'column', sm: 'row' },
          alignItems: { xs: 'flex-start', sm: 'center' },
          flexWrap: 'wrap',
          gap: 1.5,
          width: { xs: '100%', md: 'auto' }
        }}
      >
        <Select value={questionFilter} onChange={setQuestionFilter} size="small" sx={{ minWidth: 150 }}>
          <MenuItem value="all">{t('textSplit.allChunks')}</MenuItem>
          <MenuItem value="generated">{t('textSplit.generatedQuestions2')}</MenuItem>
          <MenuItem value="ungenerated">{t('textSplit.ungeneratedQuestions')}</MenuItem>
        </Select>

        <Box
          sx={{
            display: 'flex',
            flexWrap: 'wrap',
            gap: 1.5,
            mt: { xs: 1, sm: 0 },
            width: { xs: '100%', sm: 'auto' }
          }}
        >
          <Button
            variant="contained"
            color="primary"
            startIcon={<QuizIcon />}
            disabled={selectedChunks.length === 0}
            onClick={onBatchGenerateQuestions}
            size="medium"
            sx={{ minWidth: { xs: '48%', sm: 'auto' } }}
          >
            {t('textSplit.batchGenerateQuestions')}
          </Button>

          <Tooltip
            title={t('textSplit.autoGenerateQuestionsTip', {
              defaultValue: '创建后台批量处理任务：自动查询待生成问题的文本块并提取问题'
            })}
          >
            <Button
              variant="outlined"
              color="secondary"
              startIcon={<AutoFixHighIcon />}
              onClick={() => handleCreateAutoQuestionTask()}
              disabled={!projectId || !selectedModel?.id}
              size="medium"
              sx={{ minWidth: { xs: '48%', sm: 'auto' } }}
            >
              {t('textSplit.autoGenerateQuestions')}
            </Button>
          </Tooltip>

          {/* 更多菜单按钮 */}
          <Tooltip title={t('common.more', { defaultValue: '更多操作' })}>
            <IconButton
              onClick={handleMoreMenuClick}
              color="primary"
              size="medium"
              sx={{
                border: '1px solid',
                borderColor: 'divider'
              }}
            >
              <MoreVertIcon />
            </IconButton>
          </Tooltip>

          {/* 更多操作下拉菜单 */}
          <Menu
            anchorEl={moreMenuAnchorEl}
            open={isMoreMenuOpen}
            onClose={handleMoreMenuClose}
            anchorOrigin={{
              vertical: 'bottom',
              horizontal: 'right'
            }}
            transformOrigin={{
              vertical: 'top',
              horizontal: 'right'
            }}
          >
            <MenuItem onClick={handleBatchEdit} disabled={selectedChunks.length === 0}>
              <EditIcon fontSize="small" sx={{ mr: 1 }} />
              {t('batchEdit.batchEdit', { defaultValue: '批量编辑' })}
            </MenuItem>
            <MenuItem onClick={handleExport} disabled={chunks.length === 0}>
              <DownloadIcon fontSize="small" sx={{ mr: 1 }} />
              {t('textSplit.exportChunks', { defaultValue: '导出文本块' })}
            </MenuItem>
          </Menu>
        </Box>
      </Box>
    </Box>
  );
}

'use client';

import { useState } from 'react';
import { Box, Paper, Typography, CircularProgress, Pagination, Grid } from '@mui/material';
import ChunkListHeader from './ChunkListHeader';
import ChunkCard from './ChunkCard';
import ChunkViewDialog from './ChunkViewDialog';
import ChunkDeleteDialog from './ChunkDeleteDialog';
import BatchEditChunksDialog from './BatchEditChunkDialog';
import { useTheme } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';

/**
 * Chunk list component
 * @param {Object} props
 * @param {string} props.projectId - Project ID
 * @param {Array} props.chunks - Chunk array
 * @param {Function} props.onDelete - Delete callback
 * @param {Function} props.onEdit - Edit callback
 * @param {Function} props.onGenerateQuestions - Generate questions callback
 * @param {string} props.questionFilter - Question filter
 * @param {Function} props.onQuestionFilterChange - Question filter change callback
 * @param {Object} props.selectedModel - 选中的模型信息
 */
export default function ChunkList({
  projectId,
  chunks = [],
  onDelete,
  onEdit,
  onGenerateQuestions,
  loading = false,
  questionFilter,
  setQuestionFilter,
  selectedModel,
  onChunksUpdate
}) {
  const theme = useTheme();
  const [page, setPage] = useState(1);
  const [selectedChunks, setSelectedChunks] = useState([]);
  const [viewChunk, setViewChunk] = useState(null);
  const [viewDialogOpen, setViewDialogOpen] = useState(false);
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  const [chunkToDelete, setChunkToDelete] = useState(null);
  const [batchEditDialogOpen, setBatchEditDialogOpen] = useState(false);
  const [batchEditLoading, setBatchEditLoading] = useState(false);

  // 对文本块进行排序，先按文件ID排序，再按part-后面的数字排序
  const sortedChunks = [...chunks].sort((a, b) => {
    // 先按fileId排序
    if (a.fileId !== b.fileId) {
      return a.fileId.localeCompare(b.fileId);
    }

    // 同一文件内，再按part-后面的数字排序
    const getPartNumber = name => {
      const match = name.match(/part-(\d+)/);
      return match ? parseInt(match[1], 10) : 0;
    };

    const numA = getPartNumber(a.name);
    const numB = getPartNumber(b.name);

    return numA - numB;
  });

  const itemsPerPage = 5;
  const startIndex = (page - 1) * itemsPerPage;
  const endIndex = startIndex + itemsPerPage;
  const displayedChunks = sortedChunks.slice(startIndex, endIndex);
  const totalPages = Math.ceil(sortedChunks.length / itemsPerPage);
  const { t } = useTranslation();

  const handlePageChange = (event, value) => {
    setPage(value);
  };

  const handleViewChunk = async chunkId => {
    try {
      const response = await fetch(`/api/projects/${projectId}/chunks/${chunkId}`);
      if (!response.ok) {
        throw new Error(t('textSplit.fetchChunksFailed'));
      }

      const data = await response.json();
      setViewChunk(data);
      setViewDialogOpen(true);
    } catch (error) {
      console.error(t('textSplit.fetchChunksError'), error);
    }
  };

  const handleCloseViewDialog = () => {
    setViewDialogOpen(false);
  };

  const handleOpenDeleteDialog = chunkId => {
    setChunkToDelete(chunkId);
    setDeleteDialogOpen(true);
  };

  const handleCloseDeleteDialog = () => {
    setDeleteDialogOpen(false);
    setChunkToDelete(null);
  };

  const handleConfirmDelete = () => {
    if (chunkToDelete && onDelete) {
      onDelete(chunkToDelete);
    }
    handleCloseDeleteDialog();
  };

  // 处理编辑文本块
  const handleEditChunk = async (chunkId, newContent) => {
    if (onEdit) {
      onEdit(chunkId, newContent);
      onChunksUpdate();
    }
  };

  // 处理选择文本块
  const handleSelectChunk = chunkId => {
    setSelectedChunks(prev => {
      if (prev.includes(chunkId)) {
        return prev.filter(id => id !== chunkId);
      } else {
        return [...prev, chunkId];
      }
    });
  };

  const handleSelectAll = () => {
    if (selectedChunks.length === chunks.length) {
      setSelectedChunks([]);
    } else {
      setSelectedChunks(chunks.map(chunk => chunk.id));
    }
  };

  const handleBatchGenerateQuestions = () => {
    if (onGenerateQuestions && selectedChunks.length > 0) {
      onGenerateQuestions(selectedChunks);
    }
  };

  const handleBatchEdit = async editData => {
    try {
      setBatchEditLoading(true);

      // 调用批量编辑API
      const response = await fetch(`/api/projects/${projectId}/chunks/batch-edit`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          position: editData.position,
          content: editData.content,
          chunkIds: editData.chunkIds
        })
      });

      if (!response.ok) {
        throw new Error('批量编辑失败');
      }

      const result = await response.json();

      if (result.success) {
        // 编辑成功后，刷新文本块数据
        if (onChunksUpdate) {
          onChunksUpdate();
        }

        // 清空选中状态
        setSelectedChunks([]);

        // 关闭对话框
        setBatchEditDialogOpen(false);

        // 显示成功消息
        console.log(`成功更新了 ${result.updatedCount} 个文本块`);
      } else {
        throw new Error(result.message || '批量编辑失败');
      }
    } catch (error) {
      console.error('批量编辑失败:', error);
      // 这里可以添加错误提示
    } finally {
      setBatchEditLoading(false);
    }
  };

  // 打开批量编辑对话框
  const handleOpenBatchEdit = () => {
    setBatchEditDialogOpen(true);
  };

  // 关闭批量编辑对话框
  const handleCloseBatchEdit = () => {
    setBatchEditDialogOpen(false);
  };

  if (loading) {
    return (
      <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
        <CircularProgress />
      </Box>
    );
  }

  return (
    <Box>
      <ChunkListHeader
        projectId={projectId}
        totalChunks={chunks.length}
        selectedChunks={selectedChunks}
        onSelectAll={handleSelectAll}
        onBatchGenerateQuestions={handleBatchGenerateQuestions}
        onBatchEditChunks={handleOpenBatchEdit}
        questionFilter={questionFilter}
        setQuestionFilter={event => setQuestionFilter(event.target.value)}
        chunks={chunks}
        selectedModel={selectedModel}
      />

      <Grid container spacing={2}>
        {displayedChunks.map(chunk => (
          <Grid item xs={12} key={chunk.id}>
            <ChunkCard
              chunk={chunk}
              selected={selectedChunks.includes(chunk.id)}
              onSelect={() => handleSelectChunk(chunk.id)}
              onView={() => handleViewChunk(chunk.id)}
              onDelete={() => handleOpenDeleteDialog(chunk.id)}
              onEdit={handleEditChunk}
              onGenerateQuestions={() => onGenerateQuestions && onGenerateQuestions([chunk.id])}
              projectId={projectId}
              selectedModel={selectedModel}
            />
          </Grid>
        ))}
      </Grid>

      {chunks.length === 0 && (
        <Paper
          sx={{
            p: 4,
            textAlign: 'center',
            border: `1px solid ${theme.palette.divider}`,
            borderRadius: 2
          }}
        >
          <Typography variant="body1" color="textSecondary">
            {t('textSplit.noChunks')}
          </Typography>
        </Paper>
      )}

      {totalPages > 1 && (
        <Box sx={{ display: 'flex', justifyContent: 'center', mt: 3 }}>
          <Pagination count={totalPages} page={page} onChange={handlePageChange} color="primary" />
        </Box>
      )}

      {/* 文本块详情对话框 */}
      <ChunkViewDialog open={viewDialogOpen} chunk={viewChunk} onClose={handleCloseViewDialog} />

      {/* 删除确认对话框 */}
      <ChunkDeleteDialog open={deleteDialogOpen} onClose={handleCloseDeleteDialog} onConfirm={handleConfirmDelete} />

      {/* 批量编辑对话框 */}
      <BatchEditChunksDialog
        open={batchEditDialogOpen}
        onClose={handleCloseBatchEdit}
        onConfirm={handleBatchEdit}
        selectedChunks={selectedChunks}
        totalChunks={chunks.length}
        loading={batchEditLoading}
      />
    </Box>
  );
}

'use server';

import path from 'path';
import { getProjectRoot, readJsonFile, writeJsonFile } from './base';
import { db } from '@/lib/db/index';
import fs from 'fs';

// 获取标签树
export async function getTags(projectId) {
  try {
    return await getTagsTreeWithQuestionCount(projectId);
  } catch (error) {
    return [];
  }
}

// 递归查询树状结构，并统计问题数量
async function getTagsTreeWithQuestionCount(projectId, parentId = null) {
  // 查询当前层级的分类
  const tags = await db.tags.findMany({
    where: { parentId, projectId }
  });

  // 遍历每个分类，递归查询子节点
  for (const tag of tags) {
    // 获取当前分类及其子分类的所有 label
    const labels = await getAllLabels(tag.id);

    // 统计当前分类及其子分类的问题数量
    tag.questionCount = await db.questions.count({
      where: { label: { in: labels }, projectId }
    });

    // 递归查询子节点
    tag.child = await getTagsTreeWithQuestionCount(projectId, tag.id);
  }

  return tags;
}

// 获取某个分类及其所有子分类的 label
async function getAllLabels(tagId) {
  const labels = [];
  const queue = [tagId];

  while (queue.length > 0) {
    const currentId = queue.shift();
    const tag = await db.tags.findUnique({
      where: { id: currentId }
    });

    if (tag) {
      labels.push(tag.label);
      // 获取子分类的 ID，加入队列
      const children = await db.tags.findMany({
        where: { parentId: currentId },
        select: { id: true }
      });
      queue.push(...children.map(child => child.id));
    }
  }

  return labels;
}

export async function createTag(projectId, label, parentId) {
  try {
    let data = {
      projectId,
      label
    };
    if (parentId) {
      data.parentId = parentId;
    }
    return await db.tags.create({ data });
  } catch (error) {
    console.error('Error insert tags db:', error);
    throw error;
  }
}

export async function updateTag(label, id) {
  try {
    return await db.tags.update({ where: { id }, data: { label } });
  } catch (error) {
    console.error('Error update tags db:', error);
    throw error;
  }
}

/**
 * 删除标签及其所有子标签、问题和数据集
 * @param {string} id - 要删除的标签 ID
 * @returns {Promise<object>} 删除结果
 */
export async function deleteTag(id) {
  try {
    console.log(`开始删除标签: ${id}`);

    // 1. 获取要删除的标签
    const tag = await db.tags.findUnique({
      where: { id }
    });

    if (!tag) {
      throw new Error(`标签不存在: ${id}`);
    }

    // 2. 获取所有子标签（所有层级）
    const allChildTags = await getAllChildTags(id, tag.projectId);
    console.log(`找到 ${allChildTags.length} 个子标签需要删除`);

    // 3. 从叶子节点开始删除，防止外键约束问题
    for (const childTag of allChildTags.reverse()) {
      // 删除与标签相关的数据集
      await deleteDatasetsByTag(childTag.label, childTag.projectId);

      // 删除与标签相关的问题
      await deleteQuestionsByTag(childTag.label, childTag.projectId);

      // 删除标签
      await db.tags.delete({ where: { id: childTag.id } });
      console.log(`删除子标签: ${childTag.id} (${childTag.label})`);
    }

    // 4. 删除与当前标签相关的数据集
    await deleteDatasetsByTag(tag.label, tag.projectId);

    // 5. 删除与当前标签相关的问题
    await deleteQuestionsByTag(tag.label, tag.projectId);

    // 6. 删除当前标签
    console.log(`删除主标签: ${id} (${tag.label})`);
    return await db.tags.delete({ where: { id } });
  } catch (error) {
    console.error('删除标签时出错:', error);
    throw error;
  }
}

/**
 * 获取标签的所有子标签（所有层级）
 * @param {string} parentId - 父标签 ID
 * @param {string} projectId - 项目 ID
 * @returns {Promise<Array>} 所有子标签列表
 */
async function getAllChildTags(parentId, projectId) {
  const result = [];

  // 递归获取子标签
  async function fetchChildTags(pid) {
    // 查询直接子标签
    const children = await db.tags.findMany({
      where: {
        parentId: pid,
        projectId
      }
    });

    // 如果有子标签
    if (children.length > 0) {
      // 将子标签添加到结果中
      result.push(...children);

      // 递归获取每个子标签的子标签
      for (const child of children) {
        await fetchChildTags(child.id);
      }
    }
  }

  // 开始递归获取
  await fetchChildTags(parentId);

  return result;
}

/**
 * 删除与标签相关的问题
 * @param {string} label - 标签名称
 * @param {string} projectId - 项目 ID
 */
async function deleteQuestionsByTag(label, projectId) {
  try {
    // 查找并删除与标签相关的所有问题
    await db.questions.deleteMany({
      where: {
        label,
        projectId
      }
    });
  } catch (error) {
    console.error(`删除标签 "${label}" 相关问题时出错:`, error);
    throw error;
  }
}

/**
 * 删除与标签相关的数据集
 * @param {string} label - 标签名称
 * @param {string} projectId - 项目 ID
 */
async function deleteDatasetsByTag(label, projectId) {
  try {
    // 查找并删除与标签相关的所有数据集
    await db.datasets.deleteMany({
      where: {
        questionLabel: label,
        projectId
      }
    });
  } catch (error) {
    console.error(`删除标签 "${label}" 相关数据集时出错:`, error);
    throw error;
  }
}

// 保存整个标签树
export async function batchSaveTags(projectId, tags) {
  try {
    // 仅在入口函数删除所有标签，避免递归中重复删除
    await db.tags.deleteMany({ where: { projectId } });
    // 处理标签树
    await insertTags(projectId, tags);
  } catch (error) {
    console.error('Error insert tags db:', error);
    throw error;
  }
}

async function insertTags(projectId, tags, parentId = null) {
  // 删除操作已移至外层函数，这里不再需要
  for (const tag of tags) {
    // 插入当前节点
    const createdTag = await db.tags.create({
      data: {
        projectId,
        label: tag.label,
        parentId: parentId
      }
    });
    // 如果有子节点，递归插入
    if (tag.child && tag.child.length > 0) {
      await insertTags(projectId, tag.child, createdTag.id);
    }
  }
}

import { useState, useEffect } from 'react';

// 存储文件处理状态的共享对象
const fileProcessingSubscribers = {
  value: false,
  listeners: new Set()
};

// 存储文件任务信息的共享对象
const fileTaskSubscribers = {
  value: null,
  listeners: new Set()
};

/**
 * 自定义hook，用于在组件间共享文件处理任务的状态
 */
export default function useFileProcessingStatus() {
  const [taskFileProcessing, setTaskFileProcessing] = useState(fileProcessingSubscribers.value);
  const [task, setTask] = useState(fileTaskSubscribers.value);

  useEffect(() => {
    // 添加当前组件为订阅者
    const updateProcessingState = newValue => setTaskFileProcessing(newValue);
    const updateTaskState = newTask => setTask(newTask);

    fileProcessingSubscribers.listeners.add(updateProcessingState);
    fileTaskSubscribers.listeners.add(updateTaskState);

    // 组件卸载时清理
    return () => {
      fileProcessingSubscribers.listeners.delete(updateProcessingState);
      fileTaskSubscribers.listeners.delete(updateTaskState);
    };
  }, []);

  // 共享的setState函数
  const setSharedFileProcessing = newValue => {
    fileProcessingSubscribers.value = newValue;
    // 通知所有订阅者
    fileProcessingSubscribers.listeners.forEach(listener => listener(newValue));
  };

  // 共享的setTask函数
  const setSharedTask = newTask => {
    fileTaskSubscribers.value = newTask;
    // 通知所有订阅者
    fileTaskSubscribers.listeners.forEach(listener => listener(newTask));
  };

  return {
    taskFileProcessing,
    task,
    setTaskFileProcessing: setSharedFileProcessing,
    setTask: setSharedTask
  };
}

'use server';

import fs from 'fs';
import path from 'path';
import { getProjectRoot, ensureDir } from './base';

// 获取项目中所有原始文件
export async function getFiles(projectId) {
  const projectRoot = await getProjectRoot();
  const projectPath = path.join(projectRoot, projectId);
  const filesDir = path.join(projectPath, 'files');
  await fs.promises.access(filesDir);
  const files = await fs.promises.readdir(filesDir);
  const fileStats = await Promise.all(
    files.map(async fileName => {
      // 跳过非文件项目
      const filePath = path.join(filesDir, fileName);
      const stats = await fs.promises.stat(filePath);

      // 只返回Markdown文件，跳过其他文件
      if (!fileName.endsWith('.md')) {
        return null;
      }

      return {
        name: fileName,
        path: filePath,
        size: stats.size,
        createdAt: stats.birthtime
      };
    })
  );
  return fileStats.filter(Boolean); // 过滤掉null值
}

// 删除项目中的原始文件及相关的文本块
export async function deleteFile(projectId, fileName) {
  const projectRoot = await getProjectRoot();
  const projectPath = path.join(projectRoot, projectId);
  const filesDir = path.join(projectPath, 'files');
  const chunksDir = path.join(projectPath, 'chunks');
  const tocDir = path.join(projectPath, 'toc');

  // 确保目录存在
  await ensureDir(tocDir);

  // 删除原始文件
  const filePath = path.join(filesDir, fileName);
  try {
    await fs.promises.access(filePath);
    await fs.promises.unlink(filePath);
  } catch (error) {
    console.error(`删除文件 ${fileName} 失败:`, error);
    // 如果文件不存在，继续处理
  }

  // 删除相关的TOC文件
  const baseName = path.basename(fileName, path.extname(fileName));
  const tocPath = path.join(tocDir, `${baseName}-toc.json`);
  console.log(111, tocPath);
  try {
    await fs.promises.access(tocPath);
    await fs.promises.unlink(tocPath);
  } catch (error) {
    // 如果TOC文件不存在，继续处理
  }

  // 删除相关的文本块
  try {
    await fs.promises.access(chunksDir);
    const chunks = await fs.promises.readdir(chunksDir);

    // 过滤出与该文件相关的文本块
    const relatedChunks = chunks.filter(chunk => chunk.startsWith(`${baseName}-part-`) && chunk.endsWith('.txt'));

    // 删除相关的文本块
    for (const chunk of relatedChunks) {
      const chunkPath = path.join(chunksDir, chunk);
      await fs.promises.unlink(chunkPath);
    }
  } catch (error) {
    console.error(`删除文件 ${fileName} 相关的文本块失败:`, error);
  }

  return { success: true, fileName };
}

'use client';

import { useState, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Typography, List } from '@mui/material';
import axios from 'axios';
import { useGenerateDataset } from '@/hooks/useGenerateDataset';

// 导入子组件
import TagTreeItem from './TagTreeItem';
import TagMenu from './TagMenu';
import ConfirmDialog from './ConfirmDialog';
import { sortTagsByNumber } from './utils';

/**
 * 蒸馏树形视图组件
 * @param {Object} props
 * @param {string} props.projectId - 项目ID
 * @param {Array} props.tags - 标签列表
 * @param {Function} props.onGenerateSubTags - 生成子标签的回调函数
 * @param {Function} props.onGenerateQuestions - 生成问题的回调函数
 */
const DistillTreeView = forwardRef(function DistillTreeView(
  { projectId, tags = [], onGenerateSubTags, onGenerateQuestions },
  ref
) {
  const { t } = useTranslation();
  const [expandedTags, setExpandedTags] = useState({});
  const [tagQuestions, setTagQuestions] = useState({});
  const [loadingTags, setLoadingTags] = useState({});
  const [loadingQuestions, setLoadingQuestions] = useState({});
  const [menuAnchorEl, setMenuAnchorEl] = useState(null);
  const [selectedTagForMenu, setSelectedTagForMenu] = useState(null);
  const [allQuestions, setAllQuestions] = useState([]);
  const [loading, setLoading] = useState(false);
  const [processingQuestions, setProcessingQuestions] = useState({});
  const [deleteQuestionConfirmOpen, setDeleteQuestionConfirmOpen] = useState(false);
  const [questionToDelete, setQuestionToDelete] = useState(null);
  const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
  const [tagToDelete, setTagToDelete] = useState(null);
  const [project, setProject] = useState(null);
  const [projectName, setProjectName] = useState('');

  // 使用生成数据集的hook
  const { generateSingleDataset } = useGenerateDataset();

  // 获取问题统计信息
  const fetchQuestionsStats = useCallback(async () => {
    try {
      setLoading(true);
      const response = await axios.get(`/api/projects/${projectId}/questions/tree?isDistill=true`);
      setAllQuestions(response.data);
      console.log('获取问题统计信息成功:', { totalQuestions: response.data.length });
    } catch (error) {
      console.error('获取问题统计信息失败:', error);
    } finally {
      setLoading(false);
    }
  }, [projectId]);

  // 暴露方法给父组件
  useImperativeHandle(ref, () => ({
    fetchQuestionsStats
  }));

  // 获取标签下的问题
  const fetchQuestionsByTag = useCallback(
    async tagId => {
      try {
        setLoadingQuestions(prev => ({ ...prev, [tagId]: true }));
        const response = await axios.get(`/api/projects/${projectId}/distill/questions/by-tag?tagId=${tagId}`);
        setTagQuestions(prev => ({
          ...prev,
          [tagId]: response.data
        }));
      } catch (error) {
        console.error('获取标签问题失败:', error);
      } finally {
        setLoadingQuestions(prev => ({ ...prev, [tagId]: false }));
      }
    },
    [projectId]
  );

  // 获取项目信息，获取项目名称
  useEffect(() => {
    if (projectId) {
      axios
        .get(`/api/projects/${projectId}`)
        .then(response => {
          setProject(response.data);
          setProjectName(response.data.name || '');
        })
        .catch(error => {
          console.error('获取项目信息失败:', error);
        });
    }
  }, [projectId]);

  // 初始化时获取问题统计信息
  useEffect(() => {
    fetchQuestionsStats();
  }, [fetchQuestionsStats]);

  // 构建标签树
  const tagTree = useMemo(() => {
    const rootTags = [];
    const tagMap = {};

    // 创建标签映射
    tags.forEach(tag => {
      tagMap[tag.id] = { ...tag, children: [] };
    });

    // 构建树结构
    tags.forEach(tag => {
      if (tag.parentId && tagMap[tag.parentId]) {
        tagMap[tag.parentId].children.push(tagMap[tag.id]);
      } else {
        rootTags.push(tagMap[tag.id]);
      }
    });

    return rootTags;
  }, [tags]);

  // 切换标签展开/折叠状态
  const toggleTag = useCallback(
    tagId => {
      setExpandedTags(prev => ({
        ...prev,
        [tagId]: !prev[tagId]
      }));

      // 如果展开且还没有加载过问题，则加载问题
      if (!expandedTags[tagId] && !tagQuestions[tagId]) {
        fetchQuestionsByTag(tagId);
      }
    },
    [expandedTags, tagQuestions, fetchQuestionsByTag]
  );

  // 处理菜单打开
  const handleMenuOpen = (event, tag) => {
    event.stopPropagation();
    setMenuAnchorEl(event.currentTarget);
    setSelectedTagForMenu(tag);
  };

  // 处理菜单关闭
  const handleMenuClose = () => {
    setMenuAnchorEl(null);
    setSelectedTagForMenu(null);
  };

  // 打开删除确认对话框
  const openDeleteConfirm = () => {
    console.log('打开删除确认对话框', selectedTagForMenu);
    // 保存要删除的标签
    setTagToDelete(selectedTagForMenu);
    setDeleteConfirmOpen(true);
    handleMenuClose();
  };

  // 关闭删除确认对话框
  const closeDeleteConfirm = () => {
    setDeleteConfirmOpen(false);
  };

  // 处理删除标签
  const handleDeleteTag = () => {
    if (!tagToDelete) {
      console.log('没有要删除的标签信息');
      return;
    }

    console.log('开始删除标签:', tagToDelete.id, tagToDelete.label);

    // 先关闭确认对话框
    closeDeleteConfirm();

    // 执行删除操作
    const deleteTagAction = async () => {
      try {
        console.log('发送删除请求:', `/api/projects/${projectId}/tags?id=${tagToDelete.id}`);

        // 发送删除请求
        const response = await axios.delete(`/api/projects/${projectId}/tags?id=${tagToDelete.id}`);

        console.log('删除标签成功:', response.data);

        // 刷新页面
        window.location.reload();
      } catch (error) {
        console.error('删除标签失败:', error);
        console.error('错误详情:', error.response ? error.response.data : '无响应数据');
        alert(`删除标签失败: ${error.message}`);
      }
    };

    // 立即执行删除操作
    deleteTagAction();
  };

  // 打开删除问题确认对话框
  const openDeleteQuestionConfirm = (questionId, event) => {
    event.stopPropagation();
    setQuestionToDelete(questionId);
    setDeleteQuestionConfirmOpen(true);
  };

  // 关闭删除问题确认对话框
  const closeDeleteQuestionConfirm = () => {
    setDeleteQuestionConfirmOpen(false);
    setQuestionToDelete(null);
  };

  // 处理删除问题
  const handleDeleteQuestion = async () => {
    if (!questionToDelete) return;

    try {
      await axios.delete(`/api/projects/${projectId}/questions/${questionToDelete}`);
      // 更新问题列表
      setTagQuestions(prev => {
        const newQuestions = { ...prev };
        Object.keys(newQuestions).forEach(tagId => {
          newQuestions[tagId] = newQuestions[tagId].filter(q => q.id !== questionToDelete);
        });
        return newQuestions;
      });
      // 关闭确认对话框
      closeDeleteQuestionConfirm();
    } catch (error) {
      console.error('删除问题失败:', error);
    }
  };

  // 处理生成数据集
  const handleGenerateDataset = async (questionId, questionInfo, event) => {
    event.stopPropagation();
    // 设置处理状态
    setProcessingQuestions(prev => ({
      ...prev,
      [questionId]: true
    }));
    await generateSingleDataset({ projectId, questionId, questionInfo });
    // 重置处理状态
    setProcessingQuestions(prev => ({
      ...prev,
      [questionId]: false
    }));
  };

  // 获取标签路径
  const getTagPath = useCallback(
    tag => {
      if (!tag) return '';

      const findPath = (currentTag, path = []) => {
        const newPath = [currentTag.label, ...path];

        if (!currentTag.parentId) {
          // 如果是顶级标签，确保路径以项目名称开始
          if (projectName && !newPath.includes(projectName)) {
            return [projectName, ...newPath];
          }
          return newPath;
        }

        const parentTag = tags.find(t => t.id === currentTag.parentId);
        if (!parentTag) {
          // 如果没有找到父标签，确保路径以项目名称开始
          if (projectName && !newPath.includes(projectName)) {
            return [projectName, ...newPath];
          }
          return newPath;
        }

        return findPath(parentTag, newPath);
      };

      const path = findPath(tag);

      // 最终检查，确保路径以项目名称开始
      if (projectName && path.length > 0 && path[0] !== projectName) {
        path.unshift(projectName);
      }

      return path.join(' > ');
    },
    [tags, projectName]
  );

  // 渲染标签树
  const renderTagTree = (tagList, level = 0) => {
    // 对同级标签进行排序
    const sortedTagList = sortTagsByNumber(tagList);

    return (
      <List disablePadding sx={{ px: 2 }}>
        {sortedTagList.map(tag => (
          <TagTreeItem
            key={tag.id}
            tag={tag}
            level={level}
            expanded={expandedTags[tag.id]}
            onToggle={toggleTag}
            onMenuOpen={handleMenuOpen}
            onGenerateQuestions={tag => {
              // 包装函数，处理问题生成后的刷新
              const handleGenerateQuestionsWithRefresh = async () => {
                // 调用父组件传入的函数生成问题
                await onGenerateQuestions(tag, getTagPath(tag));

                // 生成问题后刷新数据
                await fetchQuestionsStats();

                // 如果标签已展开，刷新该标签的问题详情
                if (expandedTags[tag.id]) {
                  await fetchQuestionsByTag(tag.id);
                }
              };

              handleGenerateQuestionsWithRefresh();
            }}
            onGenerateSubTags={tag => onGenerateSubTags(tag, getTagPath(tag))}
            questions={tagQuestions[tag.id] || []}
            loadingQuestions={loadingQuestions[tag.id]}
            processingQuestions={processingQuestions}
            onDeleteQuestion={openDeleteQuestionConfirm}
            onGenerateDataset={handleGenerateDataset}
            allQuestions={allQuestions}
            tagQuestions={tagQuestions}
          >
            {/* 递归渲染子标签 */}
            {tag.children && tag.children.length > 0 && expandedTags[tag.id] && renderTagTree(tag.children, level + 1)}
          </TagTreeItem>
        ))}
      </List>
    );
  };

  return (
    <Box>
      {tagTree.length > 0 ? (
        renderTagTree(tagTree)
      ) : (
        <Box sx={{ p: 2, textAlign: 'center' }}>
          <Typography variant="body1" color="text.secondary">
            {t('distill.noTags')}
          </Typography>
        </Box>
      )}

      {/* 标签操作菜单 */}
      <TagMenu
        anchorEl={menuAnchorEl}
        open={Boolean(menuAnchorEl)}
        onClose={handleMenuClose}
        onDelete={openDeleteConfirm}
      />

      {/* 删除标签确认对话框 */}
      <ConfirmDialog
        open={deleteConfirmOpen}
        onClose={closeDeleteConfirm}
        onConfirm={handleDeleteTag}
        title={t('distill.deleteTagConfirmTitle')}
        cancelText={t('common.cancel')}
        confirmText={t('common.delete')}
        confirmColor="error"
      />

      {/* 删除问题确认对话框 */}
      <ConfirmDialog
        open={deleteQuestionConfirmOpen}
        onClose={closeDeleteQuestionConfirm}
        onConfirm={handleDeleteQuestion}
        title={t('questions.deleteConfirm')}
        cancelText={t('common.cancel')}
        confirmText={t('common.delete')}
        confirmColor="error"
      />
    </Box>
  );
});

export default DistillTreeView;

import { NextResponse } from 'next/server';
import { saveChunks, deleteChunksByFileId } from '@/lib/db/chunks';
import path from 'path';
import fs from 'fs/promises';
import { getProjectRoot } from '@/lib/db/base';

/**
 * 处理自定义分块请求
 * @param {Request} request - 请求对象
 * @param {Object} params - 路由参数
 * @returns {Promise<Response>} - 响应对象
 */
export async function POST(request, { params }) {
  try {
    const { projectId } = params;
    const { fileId, fileName, content, splitPoints } = await request.json();

    // 参数验证
    if (!projectId || !fileId || !fileName || !content || !splitPoints) {
      return NextResponse.json({ error: 'Missing required parameters' }, { status: 400 });
    }

    // 获取项目根目录
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);

    // 检查项目是否存在
    try {
      await fs.access(projectPath);
    } catch (error) {
      return NextResponse.json({ error: 'Project does not exist' }, { status: 404 });
    }

    // 先删除该文件已有的文本块
    await deleteChunksByFileId(projectId, fileId);

    // 根据分块点将文件内容分割成多个块
    const customChunks = generateCustomChunks(projectId, fileId, fileName, content, splitPoints);

    // 保存新的文本块
    await saveChunks(customChunks);

    return NextResponse.json({
      success: true,
      message: 'Custom chunks saved successfully',
      totalChunks: customChunks.length
    });
  } catch (error) {
    console.error('自定义分块处理出错:', String(error));
    return NextResponse.json({ error: error.message || 'Failed to process custom split request' }, { status: 500 });
  }
}

/**
 * 根据分块点生成自定义文本块
 * @param {string} projectId - 项目ID
 * @param {string} fileId - 文件ID
 * @param {string} fileName - 文件名
 * @param {string} content - 文件内容
 * @param {Array} splitPoints - 分块点数组
 * @returns {Array} - 生成的文本块数组
 */
function generateCustomChunks(projectId, fileId, fileName, content, splitPoints) {
  // 按位置排序分块点
  const sortedPoints = [...splitPoints].sort((a, b) => a.position - b.position);

  // 创建分块
  const chunks = [];
  let startPos = 0;

  // 处理每个分块点
  for (let i = 0; i < sortedPoints.length; i++) {
    const endPos = sortedPoints[i].position;

    // 提取当前分块内容
    const chunkContent = content.substring(startPos, endPos);

    // 跳过空白分块
    if (chunkContent.trim().length === 0) {
      startPos = endPos;
      continue;
    }

    // 创建分块对象
    const chunk = {
      projectId,
      name: `${path.basename(fileName, path.extname(fileName))}-part-${i + 1}`,
      fileId,
      fileName,
      content: chunkContent,
      summary: `${fileName} 自定义分块 ${i + 1}/${sortedPoints.length + 1}`,
      size: chunkContent.length
    };

    chunks.push(chunk);
    startPos = endPos;
  }

  // 添加最后一个分块（如果有内容）
  const lastChunkContent = content.substring(startPos);
  if (lastChunkContent.trim().length > 0) {
    const lastChunk = {
      projectId,
      name: `${path.basename(fileName, path.extname(fileName))}-part-${sortedPoints.length + 1}`,
      fileId,
      fileName,
      content: lastChunkContent,
      summary: `${fileName} 自定义分块 ${sortedPoints.length + 1}/${sortedPoints.length + 1}`,
      size: lastChunkContent.length
    };

    chunks.push(lastChunk);
  }

  return chunks;
}

'use client';

import React, { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'next/navigation';
import { useAtomValue } from 'jotai';
import { selectedModelInfoAtom } from '@/lib/store';
import { Box, Typography, Paper, Container, Button, CircularProgress, Alert, IconButton, Tooltip } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import DistillTreeView from '@/components/distill/DistillTreeView';
import TagGenerationDialog from '@/components/distill/TagGenerationDialog';
import QuestionGenerationDialog from '@/components/distill/QuestionGenerationDialog';
import AutoDistillDialog from '@/components/distill/AutoDistillDialog';
import AutoDistillProgress from '@/components/distill/AutoDistillProgress';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import { autoDistillService } from './autoDistillService';
import axios from 'axios';

export default function DistillPage() {
  const { t, i18n } = useTranslation();
  const { projectId } = useParams();
  const selectedModel = useAtomValue(selectedModelInfoAtom);

  const [project, setProject] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [tags, setTags] = useState([]);

  // 标签生成对话框相关状态
  const [tagDialogOpen, setTagDialogOpen] = useState(false);
  const [questionDialogOpen, setQuestionDialogOpen] = useState(false);
  const [selectedTag, setSelectedTag] = useState(null);
  const [selectedTagPath, setSelectedTagPath] = useState('');

  // 自动蒸馏相关状态
  const [autoDistillDialogOpen, setAutoDistillDialogOpen] = useState(false);
  const [autoDistillProgressOpen, setAutoDistillProgressOpen] = useState(false);
  const [autoDistillRunning, setAutoDistillRunning] = useState(false);
  const [distillStats, setDistillStats] = useState({
    tagsCount: 0,
    questionsCount: 0,
    datasetsCount: 0
  });
  const [distillProgress, setDistillProgress] = useState({
    stage: 'initializing',
    tagsTotal: 0,
    tagsBuilt: 0,
    questionsTotal: 0,
    questionsBuilt: 0,
    datasetsTotal: 0,
    datasetsBuilt: 0,
    logs: []
  });

  const treeViewRef = useRef(null);

  // 获取项目信息和标签列表
  useEffect(() => {
    if (projectId) {
      fetchProject();
      fetchTags();
      fetchDistillStats();
    }
  }, [projectId]);

  // 获取项目信息
  const fetchProject = async () => {
    try {
      setLoading(true);
      const response = await axios.get(`/api/projects/${projectId}`);
      setProject(response.data);
    } catch (error) {
      console.error('获取项目信息失败:', error);
      setError(t('common.fetchError'));
    } finally {
      setLoading(false);
    }
  };

  // 获取标签列表
  const fetchTags = async () => {
    try {
      setLoading(true);
      const response = await axios.get(`/api/projects/${projectId}/distill/tags/all`);
      setTags(response.data);
    } catch (error) {
      console.error('获取标签列表失败:', error);
      setError(t('common.fetchError'));
    } finally {
      setLoading(false);
    }
  };

  // 获取蒸馏统计信息
  const fetchDistillStats = async () => {
    try {
      // 获取标签数量
      const tagsResponse = await axios.get(`/api/projects/${projectId}/distill/tags/all`);
      const tagsCount = tagsResponse.data.length;

      // 获取问题数量
      const questionsResponse = await axios.get(`/api/projects/${projectId}/questions/tree?isDistill=true`);
      const questionsCount = questionsResponse.data.length;

      // TODO: 获取数据集数量，简化起见暂用问题数量代替
      const datasetsCount = questionsResponse.data.filter(q => q.answered).length;

      setDistillStats({
        tagsCount,
        questionsCount,
        datasetsCount
      });
    } catch (error) {
      console.error('获取蒸馏统计信息失败:', error);
    }
  };

  // 打开生成标签对话框
  const handleOpenTagDialog = (tag = null, tagPath = '') => {
    if (!selectedModel || Object.keys(selectedModel).length === 0) {
      setError(t('distill.selectModelFirst'));
      return;
    }
    setSelectedTag(tag);
    setSelectedTagPath(tagPath);
    setTagDialogOpen(true);
  };

  // 打开生成问题对话框
  const handleOpenQuestionDialog = (tag, tagPath) => {
    if (!selectedModel || Object.keys(selectedModel).length === 0) {
      setError(t('distill.selectModelFirst'));
      return;
    }
    setSelectedTag(tag);
    setSelectedTagPath(tagPath);
    setQuestionDialogOpen(true);
  };

  // 处理标签生成完成
  const handleTagGenerated = () => {
    fetchTags(); // 重新获取标签列表
    setTagDialogOpen(false);
  };

  // 处理问题生成完成
  const handleQuestionGenerated = () => {
    // 关闭对话框
    setQuestionDialogOpen(false);

    // 刷新标签数据
    fetchTags();
    fetchDistillStats();

    // 如果 treeViewRef 存在且有 fetchQuestionsStats 方法，则调用它刷新问题统计信息
    if (treeViewRef.current && typeof treeViewRef.current.fetchQuestionsStats === 'function') {
      treeViewRef.current.fetchQuestionsStats();
    }
  };

  // 打开自动蒸馏对话框
  const handleOpenAutoDistillDialog = () => {
    if (!selectedModel || Object.keys(selectedModel).length === 0) {
      setError(t('distill.selectModelFirst'));
      return;
    }
    setAutoDistillDialogOpen(true);
  };

  // 开始自动蒸馏任务
  const handleStartAutoDistill = async config => {
    setAutoDistillDialogOpen(false);
    setAutoDistillProgressOpen(true);
    setAutoDistillRunning(true);

    // 初始化进度信息
    setDistillProgress({
      stage: 'initializing',
      tagsTotal: config.estimatedTags,
      tagsBuilt: distillStats.tagsCount || 0,
      questionsTotal: config.estimatedQuestions,
      questionsBuilt: distillStats.questionsCount || 0,
      datasetsTotal: config.estimatedQuestions, // 初步设置数据集总数为问题数，后面会更新
      datasetsBuilt: distillStats.datasetsCount || 0, // 根据当前已生成的数据集数量初始化
      logs: [t('distill.autoDistillStarted', { time: new Date().toLocaleTimeString() })]
    });

    try {
      // 检查模型是否存在
      if (!selectedModel || Object.keys(selectedModel).length === 0) {
        addLog(t('distill.selectModelFirst'));
        setAutoDistillRunning(false);
        return;
      }

      // 使用 autoDistillService 执行蒸馏任务
      await autoDistillService.executeDistillTask({
        projectId,
        topic: config.topic,
        levels: config.levels,
        tagsPerLevel: config.tagsPerLevel,
        questionsPerTag: config.questionsPerTag,
        model: selectedModel,
        language: i18n.language,
        concurrencyLimit: project?.taskConfig?.concurrencyLimit || 5, // 从项目配置中获取并发限制
        onProgress: updateProgress,
        onLog: addLog
      });

      // 更新任务状态
      setAutoDistillRunning(false);
    } catch (error) {
      console.error('自动蒸馏任务执行失败:', error);
      addLog(`任务执行出错: ${error.message || '未知错误'}`);
      setAutoDistillRunning(false);
    }
  };

  // 更新进度
  const updateProgress = progressUpdate => {
    setDistillProgress(prev => {
      const newProgress = { ...prev };

      // 更新阶段
      if (progressUpdate.stage) {
        newProgress.stage = progressUpdate.stage;
      }

      // 更新标签总数
      if (progressUpdate.tagsTotal) {
        newProgress.tagsTotal = progressUpdate.tagsTotal;
      }

      // 更新已构建标签数
      if (progressUpdate.tagsBuilt) {
        if (progressUpdate.updateType === 'increment') {
          newProgress.tagsBuilt += progressUpdate.tagsBuilt;
        } else {
          newProgress.tagsBuilt = progressUpdate.tagsBuilt;
        }
      }

      // 更新问题总数
      if (progressUpdate.questionsTotal) {
        newProgress.questionsTotal = progressUpdate.questionsTotal;
      }

      // 更新已生成问题数
      if (progressUpdate.questionsBuilt) {
        if (progressUpdate.updateType === 'increment') {
          newProgress.questionsBuilt += progressUpdate.questionsBuilt;
        } else {
          newProgress.questionsBuilt = progressUpdate.questionsBuilt;
        }
      }

      // 更新数据集总数
      if (progressUpdate.datasetsTotal) {
        newProgress.datasetsTotal = progressUpdate.datasetsTotal;
      }

      // 更新已生成数据集数
      if (progressUpdate.datasetsBuilt) {
        if (progressUpdate.updateType === 'increment') {
          newProgress.datasetsBuilt += progressUpdate.datasetsBuilt;
        } else {
          newProgress.datasetsBuilt = progressUpdate.datasetsBuilt;
        }
      }

      return newProgress;
    });
  };

  // 添加日志，最多保留200条
  const addLog = message => {
    setDistillProgress(prev => {
      const newLogs = [...prev.logs, message];
      // 如果日志超过200条，只保留最新的200条
      const limitedLogs = newLogs.length > 200 ? newLogs.slice(-200) : newLogs;
      return {
        ...prev,
        logs: limitedLogs
      };
    });
  };

  // 关闭进度对话框
  const handleCloseProgressDialog = () => {
    if (!autoDistillRunning) {
      setAutoDistillProgressOpen(false);
      // 刷新数据
      fetchTags();
      fetchDistillStats();
      if (treeViewRef.current && typeof treeViewRef.current.fetchQuestionsStats === 'function') {
        treeViewRef.current.fetchQuestionsStats();
      }
    } else {
      // 如果任务还在运行，可以展示一个确认对话框
      // 这里简化处理，直接关闭
      setAutoDistillProgressOpen(false);
    }
  };

  if (!projectId) {
    return (
      <Container maxWidth="lg" sx={{ mt: 4 }}>
        <Alert severity="error">{t('common.projectIdRequired')}</Alert>
      </Container>
    );
  }

  return (
    <Container maxWidth="lg" sx={{ mt: 4, mb: 8 }}>
      <Paper elevation={0} sx={{ p: 3, borderRadius: 2, border: '1px solid', borderColor: 'divider' }}>
        <Box
          sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4, paddingLeft: '32px' }}
        >
          <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
            <Typography variant="h5" component="h1" fontWeight="bold">
              {t('distill.title')}
            </Typography>
            <Tooltip title={t('common.help')}>
              <IconButton
                size="small"
                onClick={() => {
                  const helpUrl =
                    i18n.language === 'en'
                      ? 'https://docs.easy-dataset.com/ed/en/advanced/images-and-media'
                      : 'https://docs.easy-dataset.com/jin-jie-shi-yong/images-and-media';
                  window.open(helpUrl, '_blank');
                }}
                sx={{ color: 'text.secondary' }}
              >
                <HelpOutlineIcon fontSize="small" />
              </IconButton>
            </Tooltip>
          </Box>
          <Box sx={{ display: 'flex', gap: 2 }}>
            <Button
              variant="outlined"
              color="primary"
              size="large"
              onClick={handleOpenAutoDistillDialog}
              disabled={!selectedModel}
              startIcon={<AutoFixHighIcon />}
              sx={{ px: 3, py: 1 }}
            >
              {t('distill.autoDistillButton')}
            </Button>
            <Button
              variant="contained"
              color="primary"
              size="large"
              onClick={() => handleOpenTagDialog(null)}
              disabled={!selectedModel}
              startIcon={<AddIcon />}
              sx={{ px: 3, py: 1 }}
            >
              {t('distill.generateRootTags')}
            </Button>
          </Box>
        </Box>

        {error && (
          <Alert severity="error" sx={{ mb: 4, px: 3, py: 2 }} onClose={() => setError('')}>
            {error}
          </Alert>
        )}

        {loading ? (
          <Box sx={{ display: 'flex', justifyContent: 'center', p: 6 }}>
            <CircularProgress size={40} />
          </Box>
        ) : (
          <Box sx={{ mt: 2 }}>
            <DistillTreeView
              ref={treeViewRef}
              projectId={projectId}
              tags={tags}
              onGenerateSubTags={handleOpenTagDialog}
              onGenerateQuestions={handleOpenQuestionDialog}
            />
          </Box>
        )}
      </Paper>

      {/* 生成标签对话框 */}
      {tagDialogOpen && (
        <TagGenerationDialog
          open={tagDialogOpen}
          onClose={() => setTagDialogOpen(false)}
          onGenerated={handleTagGenerated}
          projectId={projectId}
          parentTag={selectedTag}
          tagPath={selectedTagPath}
          model={selectedModel}
        />
      )}

      {/* 生成问题对话框 */}
      {questionDialogOpen && (
        <QuestionGenerationDialog
          open={questionDialogOpen}
          onClose={() => setQuestionDialogOpen(false)}
          onGenerated={handleQuestionGenerated}
          projectId={projectId}
          tag={selectedTag}
          tagPath={selectedTagPath}
          model={selectedModel}
        />
      )}

      {/* 全自动蒸馏数据集配置对话框 */}
      <AutoDistillDialog
        open={autoDistillDialogOpen}
        onClose={() => setAutoDistillDialogOpen(false)}
        onStart={handleStartAutoDistill}
        projectId={projectId}
        project={project}
        stats={distillStats}
      />

      {/* 全自动蒸馏进度对话框 */}
      <AutoDistillProgress
        open={autoDistillProgressOpen}
        onClose={handleCloseProgressDialog}
        progress={distillProgress}
      />
    </Container>
  );
}

const fs = require('fs');
const path = require('path');
const { dialog } = require('electron');
const { updateDatabase } = require('./db-updater');

/**
 * 清除数据库缓存
 * @param {Object} app Electron app 对象
 * @returns {Promise<boolean>} 操作是否成功
 */
async function clearDatabaseCache(app) {
  // 清理local-db目录，保留db.sqlite文件
  const localDbDir = path.join(app.getPath('userData'), 'local-db');
  if (fs.existsSync(localDbDir)) {
    // 读取目录下所有文件
    const files = await fs.promises.readdir(localDbDir);
    // 删除除了db.sqlite之外的所有文件
    for (const file of files) {
      if (file !== 'db.sqlite') {
        const filePath = path.join(localDbDir, file);
        const stat = await fs.promises.stat(filePath);
        if (stat.isFile()) {
          await fs.promises.unlink(filePath);
          global.appLog(`已删除数据库缓存文件: ${filePath}`);
        } else if (stat.isDirectory()) {
          // 如果是目录，可能需要递归删除，根据需求决定
          global.appLog(`跳过目录: ${filePath}`);
        }
      }
    }
  }
  return true;
}

/**
 * 初始化数据库
 * @param {Object} app Electron app 对象
 * @returns {Promise<Object>} 数据库配置信息
 */
async function initializeDatabase(app) {
  try {
    // 设置数据库路径
    const userDataPath = app.getPath('userData');
    const dataDir = path.join(userDataPath, 'local-db');
    const dbFilePath = path.join(dataDir, 'db.sqlite');
    const dbJSONPath = path.join(dataDir, 'db.json');
    fs.writeFileSync(path.join(process.resourcesPath, 'root-path.txt'), dataDir);

    // 确保数据目录存在
    if (!fs.existsSync(dataDir)) {
      fs.mkdirSync(dataDir, { recursive: true });
      console.log(`数据目录已创建: ${dataDir}`);
    }

    // 设置数据库连接字符串 (Prisma 格式)
    const dbConnectionString = `file:${dbFilePath}`;
    process.env.DATABASE_URL = dbConnectionString;

    // 仅在开发环境记录日志
    const logs = {
      userDataPath,
      dataDir,
      dbFilePath,
      dbConnectionString,
      dbExists: fs.existsSync(dbFilePath)
    };
    global.appLog(`数据库配置: ${JSON.stringify(logs)}`);

    if (!fs.existsSync(dbFilePath)) {
      global.appLog('数据库文件不存在，正在初始化...');

      try {
        const resourcePath =
          process.env.NODE_ENV === 'development'
            ? path.join(__dirname, '../..', 'prisma', 'template.sqlite')
            : path.join(process.resourcesPath, 'prisma', 'template.sqlite');

        const resourceJSONPath =
          process.env.NODE_ENV === 'development'
            ? path.join(__dirname, '../..', 'prisma', 'sql.json')
            : path.join(process.resourcesPath, 'prisma', 'sql.json');

        global.appLog(`resourcePath: ${resourcePath}`);

        if (fs.existsSync(resourcePath)) {
          fs.copyFileSync(resourcePath, dbFilePath);
          global.appLog(`数据库已从模板初始化: ${dbFilePath}`);
        }

        if (fs.existsSync(resourceJSONPath)) {
          fs.copyFileSync(resourceJSONPath, dbJSONPath);
          global.appLog(`数据库SQL配置已初始化: ${dbJSONPath}`);
        }
      } catch (error) {
        console.error('数据库初始化失败:', error);
        dialog.showErrorBox('数据库初始化失败', `应用无法初始化数据库，可能需要重新安装。\n错误详情: ${error.message}`);
        throw error;
      }
    } else {
      // 数据库文件存在，检查是否需要更新
      global.appLog('检查数据库是否需要更新...');
      try {
        const resourcesPath =
          process.env.NODE_ENV === 'development' ? path.join(__dirname, '../..') : process.resourcesPath;

        const isDev = process.env.NODE_ENV === 'development';

        // 更新数据库
        const result = await updateDatabase(userDataPath, resourcesPath, isDev, global.appLog);

        if (result.updated) {
          global.appLog(`数据库更新成功: ${result.message}`);
          global.appLog(`执行的版本: ${result.executedVersions.join(', ')}`);
        } else {
          global.appLog(`数据库无需更新: ${result.message}`);
        }
      } catch (error) {
        console.error('数据库更新失败:', error);
        global.appLog(`数据库更新失败: ${error.message}`, 'error');

        // 非致命错误，只提示但不阻止应用启动
        dialog.showMessageBox({
          type: 'warning',
          title: '数据库更新警告',
          message: '数据库更新过程中出现错误，部分功能可能受影响。',
          detail: `错误详情: ${error.message}\n\n您可以继续使用应用，但如果遇到问题，请重新安装应用。`,
          buttons: ['继续']
        });
      }
    }

    return {
      userDataPath,
      dataDir,
      dbFilePath,
      dbConnectionString
    };
  } catch (error) {
    console.error('初始化数据库时发生错误:', error);
    throw error;
  }
}

module.exports = {
  clearDatabaseCache,
  initializeDatabase
};

import { NextResponse } from 'next/server';
import { getProject, updateProject, getTaskConfig } from '@/lib/db/projects';

// 获取项目配置
export async function GET(request, { params }) {
  try {
    const projectId = params.projectId;
    const config = await getProject(projectId);
    const taskConfig = await getTaskConfig(projectId);
    return NextResponse.json({ ...config, ...taskConfig });
  } catch (error) {
    console.error('获取项目配置失败:', String(error));
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

// 更新项目配置
export async function PUT(request, { params }) {
  try {
    const projectId = params.projectId;
    const newConfig = await request.json();
    const currentConfig = await getProject(projectId);

    // 只更新 prompts 部分
    const updatedConfig = {
      ...currentConfig,
      ...newConfig.prompts
    };

    const config = await updateProject(projectId, updatedConfig);
    return NextResponse.json(config);
  } catch (error) {
    console.error('更新项目配置失败:', String(error));
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

'use client';

import { useState, useEffect } from 'react';
import {
  Container,
  Box,
  Typography,
  Button,
  IconButton,
  Paper,
  CircularProgress,
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  Card,
  useTheme,
  alpha,
  InputBase,
  LinearProgress,
  Select,
  MenuItem
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import SearchIcon from '@mui/icons-material/Search';
import FileDownloadIcon from '@mui/icons-material/FileDownload';
import FilterListIcon from '@mui/icons-material/FilterList';
import { useRouter } from 'next/navigation';
import ExportDatasetDialog from '@/components/ExportDatasetDialog';
import { useTranslation } from 'react-i18next';
import DatasetList from './components/DatasetList';
import useDatasetExport from './hooks/useDatasetExport';
import { processInParallel } from '@/lib/util/async';
import axios from 'axios';
import { useDebounce } from '@/hooks/useDebounce';
import { toast } from 'sonner';

// 删除确认对话框
const DeleteConfirmDialog = ({ open, datasets, onClose, onConfirm, batch, progress, deleting }) => {
  const theme = useTheme();
  const { t } = useTranslation();
  const dataset = datasets?.[0];
  return (
    <Dialog
      open={open}
      onClose={onClose}
      PaperProps={{
        elevation: 3,
        sx: { borderRadius: 2 }
      }}
    >
      <DialogTitle sx={{ pb: 1 }}>
        <Typography variant="h6" fontWeight="bold">
          {t('common.confirmDelete')}
        </Typography>
      </DialogTitle>
      <DialogContent sx={{ pb: 2, pt: 1 }}>
        <Typography variant="body1" sx={{ mb: 2 }}>
          {batch
            ? t('datasets.batchconfirmDeleteMessage', {
                count: datasets.length
              })
            : t('common.confirmDeleteDataSet')}
        </Typography>
        {batch ? (
          ''
        ) : (
          <Paper
            variant="outlined"
            sx={{
              p: 2,
              backgroundColor: alpha(theme.palette.warning.light, 0.1),
              borderColor: theme.palette.warning.light
            }}
          >
            <Typography variant="subtitle2" color="text.secondary" fontWeight="bold">
              {t('datasets.question')}：
            </Typography>
            <Typography variant="body2">{dataset?.question}</Typography>
          </Paper>
        )}
        {deleting && progress ? (
          <Box sx={{ mb: 3 }}>
            <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
              <Typography variant="body1" sx={{ mr: 1 }}>
                {progress.percentage}%
              </Typography>
              <Box sx={{ width: '100%' }}>
                <LinearProgress
                  variant="determinate"
                  value={progress.percentage}
                  sx={{
                    height: 8,
                    borderRadius: 4,
                    '& .MuiLinearProgress-bar': {
                      transitionDuration: '0.1s'
                    }
                  }}
                  color="primary"
                />
              </Box>
            </Box>
            <Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 2 }}>
              <Typography variant="body2">
                {t('datasets.batchDeleteProgress', {
                  completed: progress.completed,
                  total: progress.total
                })}
              </Typography>
              <Typography variant="body2" color="success.main" sx={{ fontWeight: 'medium' }}>
                {t('datasets.batchDeleteCount', { count: progress.datasetCount })}
              </Typography>
            </Box>
          </Box>
        ) : (
          ''
        )}
      </DialogContent>
      <DialogActions sx={{ px: 3, pb: 2 }}>
        <Button onClick={onClose} variant="outlined" sx={{ borderRadius: 2 }}>
          {t('common.cancel')}
        </Button>
        <Button onClick={onConfirm} color="error" variant="contained" sx={{ borderRadius: 2 }}>
          {t('common.delete')}
        </Button>
      </DialogActions>
    </Dialog>
  );
};

// 主页面组件
export default function DatasetsPage({ params }) {
  const { projectId } = params;
  const router = useRouter();
  const theme = useTheme();
  const [datasets, setDatasets] = useState([]);
  const [loading, setLoading] = useState(true);
  const [deleteDialog, setDeleteDialog] = useState({
    open: false,
    datasets: null,
    batch: false,
    deleting: false
  });
  const [page, setPage] = useState(1);
  const [rowsPerPage, setRowsPerPage] = useState(10);
  const [searchQuery, setSearchQuery] = useState('');
  const debouncedSearchQuery = useDebounce(searchQuery);
  const [searchField, setSearchField] = useState('question'); // 新增：筛选字段，默认为问题
  const [exportDialog, setExportDialog] = useState({ open: false });
  const [selectedIds, setselectedIds] = useState([]);
  const [filterConfirmed, setFilterConfirmed] = useState('all');
  const [filterHasCot, setFilterHasCot] = useState('all');
  const [filterDialogOpen, setFilterDialogOpen] = useState(false);
  const { t } = useTranslation();
  // 删除进度状态
  const [deleteProgress, setDeteleProgress] = useState({
    total: 0, // 总删除问题数量
    completed: 0, // 已删除完成的数量
    percentage: 0 // 进度百分比
  });

  // 3. 添加打开导出对话框的处理函数
  const handleOpenExportDialog = () => {
    setExportDialog({ open: true });
  };

  // 4. 添加关闭导出对话框的处理函数
  const handleCloseExportDialog = () => {
    setExportDialog({ open: false });
  };

  // 获取数据集列表
  const getDatasetsList = async () => {
    try {
      setLoading(true);
      const response = await axios.get(
        `/api/projects/${projectId}/datasets?page=${page}&size=${rowsPerPage}&status=${filterConfirmed}&input=${searchQuery}&field=${searchField}&hasCot=${filterHasCot}`
      );
      setDatasets(response.data);
    } catch (error) {
      toast.error(error.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    getDatasetsList();
  }, [projectId, page, rowsPerPage, filterConfirmed, debouncedSearchQuery, searchField, filterHasCot]);

  // 处理页码变化
  const handlePageChange = (event, newPage) => {
    // MUI TablePagination 的页码从 0 开始，而我们的 API 从 1 开始
    setPage(newPage + 1);
  };

  // 处理每页行数变化
  const handleRowsPerPageChange = event => {
    setPage(1);
    setRowsPerPage(parseInt(event.target.value, 10));
  };

  // 打开删除确认框
  const handleOpenDeleteDialog = dataset => {
    setDeleteDialog({
      open: true,
      datasets: [dataset]
    });
  };

  // 关闭删除确认框
  const handleCloseDeleteDialog = () => {
    setDeleteDialog({
      open: false,
      dataset: null
    });
  };

  const handleBatchDeleteDataset = async () => {
    const datasetsArray = selectedIds.map(id => ({ id }));
    setDeleteDialog({
      open: true,
      datasets: datasetsArray,
      batch: true,
      count: selectedIds.length
    });
  };

  const resetProgress = () => {
    setDeteleProgress({
      total: deleteDialog.count,
      completed: 0,
      percentage: 0
    });
  };

  const handleDeleteConfirm = async () => {
    if (deleteDialog.batch) {
      setDeleteDialog({
        ...deleteDialog,
        deleting: true
      });
      await handleBatchDelete();
      resetProgress();
    } else {
      const [dataset] = deleteDialog.datasets;
      if (!dataset) return;
      await handleDelete(dataset);
    }
    setselectedIds([]);
    // 刷新数据
    getDatasetsList();
    // 关闭确认框
    handleCloseDeleteDialog();
  };

  // 批量删除数据集
  const handleBatchDelete = async () => {
    try {
      await processInParallel(
        selectedIds,
        async datasetId => {
          await fetch(`/api/projects/${projectId}/datasets?id=${datasetId}`, {
            method: 'DELETE'
          });
        },
        3,
        (cur, total) => {
          setDeteleProgress({
            total,
            completed: cur,
            percentage: Math.floor((cur / total) * 100)
          });
        }
      );

      toast.success(t('common.deleteSuccess'));
    } catch (error) {
      console.error('批量删除失败:', error);
      toast.error(error.message || t('common.deleteFailed'));
    }
  };

  // 删除数据集
  const handleDelete = async dataset => {
    try {
      const response = await fetch(`/api/projects/${projectId}/datasets?id=${dataset.id}`, {
        method: 'DELETE'
      });
      if (!response.ok) throw new Error(t('datasets.deleteFailed'));

      toast.success(t('datasets.deleteSuccess'));
    } catch (error) {
      toast.error(error.message || t('datasets.deleteFailed'));
    }
  };

  // 使用自定义 Hook 处理数据集导出逻辑
  const { exportDatasets } = useDatasetExport(projectId);

  // 处理导出数据集
  const handleExportDatasets = async exportOptions => {
    const success = await exportDatasets(exportOptions);
    if (success) {
      // 关闭导出对话框
      handleCloseExportDialog();
    }
  };

  // 查看详情
  const handleViewDetails = id => {
    router.push(`/projects/${projectId}/datasets/${id}`);
  };

  // 处理全选/取消全选
  const handleSelectAll = async event => {
    if (event.target.checked) {
      // 获取所有符合当前筛选条件的数据，不受分页限制
      const response = await axios.get(
        `/api/projects/${projectId}/datasets?status=${filterConfirmed}&input=${searchQuery}&selectedAll=1`
      );
      setselectedIds(response.data.map(dataset => dataset.id));
    } else {
      setselectedIds([]);
    }
  };

  // 处理单个选择
  const handleSelectItem = id => {
    setselectedIds(prev => {
      if (prev.includes(id)) {
        return prev.filter(item => item !== id);
      } else {
        return [...prev, id];
      }
    });
  };

  if (loading) {
    return (
      <Container maxWidth="lg" sx={{ mt: 4 }}>
        <Box
          sx={{
            display: 'flex',
            flexDirection: 'column',
            justifyContent: 'center',
            alignItems: 'center',
            height: '70vh'
          }}
        >
          <CircularProgress size={60} thickness={4} />
          <Typography variant="h6" sx={{ mt: 2 }}>
            {t('datasets.loading')}
          </Typography>
        </Box>
      </Container>
    );
  }

  return (
    <Container maxWidth="lg" sx={{ mt: 4, mb: 6 }}>
      <Card
        elevation={0}
        sx={{
          mb: 4,
          p: 3,
          backgroundColor: alpha(theme.palette.primary.light, 0.05),
          borderRadius: 2
        }}
      >
        <Box
          sx={{
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            flexWrap: 'wrap',
            gap: 2
          }}
        >
          <Box>
            <Typography variant="h4" fontWeight="bold" sx={{ mb: 0.5 }}>
              {t('datasets.management')}
            </Typography>
            <Typography variant="body1" color="text.secondary">
              {t('datasets.stats', {
                total: datasets.total,
                confirmed: datasets.confirmedCount,
                percentage: datasets.total > 0 ? ((datasets.confirmedCount / datasets.total) * 100).toFixed(2) : 0
              })}
            </Typography>
          </Box>
          <Box sx={{ display: 'flex', gap: 2 }}>
            <Paper
              component="form"
              sx={{
                p: '2px 4px',
                display: 'flex',
                alignItems: 'center',
                width: 400,
                borderRadius: 2
              }}
            >
              <IconButton sx={{ p: '10px' }} aria-label="search">
                <SearchIcon />
              </IconButton>
              <InputBase
                sx={{ ml: 1, flex: 1 }}
                placeholder={t('datasets.searchPlaceholder')}
                value={searchQuery}
                onChange={e => {
                  setSearchQuery(e.target.value);
                  setPage(1);
                }}
                endAdornment={
                  <Select
                    value={searchField}
                    onChange={e => {
                      setSearchField(e.target.value);
                      setPage(1);
                    }}
                    variant="standard"
                    sx={{
                      minWidth: 90,
                      '& .MuiInput-underline:before': { borderBottom: 'none' },
                      '& .MuiInput-underline:after': { borderBottom: 'none' },
                      '& .MuiInput-underline:hover:not(.Mui-disabled):before': { borderBottom: 'none' }
                    }}
                    disableUnderline
                  >
                    <MenuItem value="question">{t('datasets.fieldQuestion')}</MenuItem>
                    <MenuItem value="answer">{t('datasets.fieldAnswer')}</MenuItem>
                    <MenuItem value="cot">{t('datasets.fieldCOT')}</MenuItem>
                    <MenuItem value="questionLabel">{t('datasets.fieldLabel')}</MenuItem>
                  </Select>
                }
              />
            </Paper>
            <Button
              variant="outlined"
              onClick={() => setFilterDialogOpen(true)}
              startIcon={<FilterListIcon />}
              sx={{ borderRadius: 2 }}
            >
              {t('datasets.moreFilters')}
            </Button>
            <Button
              variant="outlined"
              startIcon={<FileDownloadIcon />}
              sx={{ borderRadius: 2 }}
              onClick={handleOpenExportDialog}
            >
              {t('export.title')}
            </Button>
          </Box>
        </Box>
      </Card>
      {selectedIds.length ? (
        <Box
          sx={{
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            flexWrap: 'wrap',
            marginTop: '10px',
            gap: 2
          }}
        >
          <Typography variant="body1" color="text.secondary">
            {t('datasets.selected', {
              count: selectedIds.length
            })}
          </Typography>
          <Button
            variant="outlined"
            color="error"
            startIcon={<DeleteIcon />}
            sx={{ borderRadius: 2 }}
            onClick={handleBatchDeleteDataset}
          >
            {t('datasets.batchDelete')}
          </Button>
        </Box>
      ) : (
        ''
      )}

      <DatasetList
        datasets={datasets.data}
        onViewDetails={handleViewDetails}
        onDelete={handleOpenDeleteDialog}
        page={page}
        rowsPerPage={rowsPerPage}
        onPageChange={handlePageChange}
        onRowsPerPageChange={handleRowsPerPageChange}
        total={datasets.total}
        selectedIds={selectedIds}
        onSelectAll={handleSelectAll}
        onSelectItem={handleSelectItem}
      />

      <DeleteConfirmDialog
        open={deleteDialog.open}
        datasets={deleteDialog.datasets || []}
        onClose={handleCloseDeleteDialog}
        onConfirm={handleDeleteConfirm}
        batch={deleteDialog.batch}
        progress={deleteProgress}
        deleting={deleteDialog.deleting}
      />

      {/* 更多筛选对话框 */}
      <Dialog open={filterDialogOpen} onClose={() => setFilterDialogOpen(false)} maxWidth="xs" fullWidth>
        <DialogTitle>{t('datasets.filtersTitle')}</DialogTitle>
        <DialogContent>
          <Box sx={{ mb: 3, mt: 1 }}>
            <Typography variant="subtitle2" gutterBottom>
              {t('datasets.filterConfirmationStatus')}
            </Typography>
            <Select
              value={filterConfirmed}
              onChange={e => setFilterConfirmed(e.target.value)}
              fullWidth
              size="small"
              sx={{ mt: 1 }}
            >
              <MenuItem value="all">{t('datasets.filterAll')}</MenuItem>
              <MenuItem value="confirmed">{t('datasets.filterConfirmed')}</MenuItem>
              <MenuItem value="unconfirmed">{t('datasets.filterUnconfirmed')}</MenuItem>
            </Select>
          </Box>

          <Box sx={{ mb: 2 }}>
            <Typography variant="subtitle2" gutterBottom>
              {t('datasets.filterCotStatus')}
            </Typography>
            <Select
              value={filterHasCot}
              onChange={e => setFilterHasCot(e.target.value)}
              fullWidth
              size="small"
              sx={{ mt: 1 }}
            >
              <MenuItem value="all">{t('datasets.filterAll')}</MenuItem>
              <MenuItem value="yes">{t('datasets.filterHasCot')}</MenuItem>
              <MenuItem value="no">{t('datasets.filterNoCot')}</MenuItem>
            </Select>
          </Box>
        </DialogContent>
        <DialogActions>
          <Button
            onClick={() => {
              setFilterConfirmed('all');
              setFilterHasCot('all');
              getDatasetsList();
            }}
          >
            {t('datasets.resetFilters')}
          </Button>
          <Button
            onClick={() => {
              setFilterDialogOpen(false);
              setPage(1); // 重置到第一页
              getDatasetsList();
            }}
            variant="contained"
          >
            {t('datasets.applyFilters')}
          </Button>
        </DialogActions>
      </Dialog>

      <ExportDatasetDialog
        open={exportDialog.open}
        onClose={handleCloseExportDialog}
        onExport={handleExportDatasets}
        projectId={projectId}
      />
    </Container>
  );
}

'use server';
import { db } from '@/lib/db/index';

/**
 * 获取项目的所有问题
 * @param {string} projectId - 项目ID
 * @param {number} page - 页码
 * @param {number} pageSize - 每页大小
 * @param answered
 * @param input
 * @returns {Promise<{data: Array, total: number}>} - 问题列表和总条数
 */
export async function getQuestions(projectId, page = 1, pageSize = 10, answered, input) {
  try {
    const whereClause = {
      projectId,
      ...(answered !== undefined && { answered: answered }), // 确保 answered 是布尔值
      OR: [{ question: { contains: input } }, { label: { contains: input } }]
    };

    const [data, total] = await Promise.all([
      db.questions.findMany({
        where: whereClause,
        orderBy: {
          createAt: 'desc'
        },
        include: {
          chunk: {
            select: {
              name: true,
              content: true
            }
          }
        },
        skip: (page - 1) * pageSize,
        take: pageSize
      }),
      db.questions.count({
        where: whereClause
      })
    ]);

    // 批量查询 datasetCount
    const datasetCounts = await getDatasetCountsForQuestions(data.map(item => item.id));

    // 合并 datasetCount 到问题项中
    const questionsWithDatasetCount = data.map((item, index) => ({
      ...item,
      datasetCount: datasetCounts[index]
    }));

    return { data: questionsWithDatasetCount, total };
  } catch (error) {
    console.error('Failed to get questions by projectId in database');
    throw error;
  }
}

/**
 * 获取项目的所有问题（仅ID和标签），用于树形视图
 * @param {string} projectId - 项目ID
 * @param {string} input - 搜索关键词
 * @param {boolean} isDistill - 是否只查询蒸馏问题
 * @returns {Promise<Array>} - 问题列表（仅包含ID和标签）
 */
export async function getQuestionsForTree(projectId, input, isDistill = false) {
  try {
    console.log('[getQuestionsForTree] 参数:', { projectId, input, isDistill });

    // 如果是蒸馏问题，需要先获取蒸馏文本块
    let whereClause = {
      projectId,
      question: { contains: input || '' }
    };

    if (isDistill) {
      // 获取蒸馏文本块
      const distillChunk = await db.chunks.findFirst({
        where: {
          projectId,
          name: 'Distilled Content'
        }
      });

      if (distillChunk) {
        whereClause.chunkId = distillChunk.id;
      }
    }

    const data = await db.questions.findMany({
      where: whereClause,
      select: {
        id: true,
        label: true,
        answered: true
      },
      orderBy: {
        createAt: 'desc'
      }
    });

    return data;
  } catch (error) {
    console.error('获取树形视图问题失败:', error);
    throw error;
  }
}

/**
 * 根据标签获取项目的问题
 * @param {string} projectId - 项目ID
 * @param {string} tag - 标签名称
 * @param {string} input - 搜索关键词
 * @param {boolean} isDistill - 是否只查询蒸馏问题
 * @returns {Promise<Array>} - 问题列表
 */
export async function getQuestionsByTag(projectId, tag, input, isDistill = false) {
  try {
    const whereClause = {
      projectId
    };

    if (input) {
      whereClause.question = { contains: input };
    }

    if (tag === 'uncategorized') {
      whereClause.label = {
        in: [tag, '其他', 'Other', 'other']
      };
    } else {
      whereClause.label = { in: [tag] };
    }

    // 如果是蒸馏问题，需要先获取蒸馏文本块
    if (isDistill) {
      // 获取蒸馏文本块
      const distillChunk = await db.chunks.findFirst({
        where: {
          projectId,
          name: 'Distilled Content'
        }
      });

      if (distillChunk) {
        whereClause.chunkId = distillChunk.id;
      }
    }

    const data = await db.questions.findMany({
      where: whereClause,
      include: {
        chunk: {
          select: {
            name: true,
            content: true
          }
        }
      },
      orderBy: {
        createAt: 'desc'
      }
    });

    // 批量查询 datasetCount
    const datasetCounts = await getDatasetCountsForQuestions(data.map(item => item.id));

    // 合并 datasetCount 到问题项中
    const questionsWithDatasetCount = data.map((item, index) => ({
      ...item,
      datasetCount: datasetCounts[index]
    }));

    return questionsWithDatasetCount;
  } catch (error) {
    console.error(`根据标签获取问题失败 (${tag}):`, error);
    throw error;
  }
}

export async function getAllQuestionsByProjectId(projectId) {
  try {
    return await db.questions.findMany({
      where: { projectId },
      include: {
        chunk: {
          select: {
            name: true,
            content: true
          }
        }
      },
      orderBy: {
        createAt: 'desc'
      }
    });
  } catch (error) {
    console.error('Failed to get datasets ids in database');
    throw error;
  }
}

export async function getQuestionsIds(projectId, answered, input) {
  try {
    const whereClause = {
      projectId,
      ...(answered !== undefined && { answered: answered }), // 确保 answered 是布尔值
      OR: [{ question: { contains: input } }, { label: { contains: input } }]
    };
    return await db.questions.findMany({
      where: whereClause,
      select: {
        id: true
      },
      orderBy: {
        createAt: 'desc'
      }
    });
  } catch (error) {
    console.error('Failed to get datasets ids in database');
    throw error;
  }
}

export async function getQuestionsByTagName(projectId, tagName) {
  try {
    return await db.questions.findMany({
      where: {
        projectId,
        label: tagName
      },
      include: {
        chunk: {
          select: {
            name: true
          }
        }
      },
      orderBy: {
        createAt: 'desc'
      }
    });
  } catch (error) {
    console.error('Failed to get datasets ids in database');
    throw error;
  }
}

/**
 * 批量获取问题的 datasetCount
 * @param {Array<string>} questionIds - 问题ID列表
 * @returns {Promise<Array<number>>} - 每个问题的 datasetCount 列表
 */
async function getDatasetCountsForQuestions(questionIds) {
  const datasetCounts = await db.datasets.groupBy({
    by: ['questionId'],
    _count: {
      questionId: true
    },
    where: {
      questionId: {
        in: questionIds
      }
    }
  });

  // 将结果转换为 questionId 到 datasetCount 的映射
  const datasetCountMap = datasetCounts.reduce((map, item) => {
    map[item.questionId] = item._count.questionId;
    return map;
  }, {});

  // 返回与 questionIds 顺序对应的 datasetCount 列表
  return questionIds.map(id => datasetCountMap[id] || 0);
}

export async function getQuestionById(id) {
  try {
    return await db.questions.findUnique({
      where: { id }
    });
  } catch (error) {
    console.error('Failed to get questions by name in database');
    throw error;
  }
}

export async function isExistByQuestion(question) {
  try {
    const count = await db.questions.count({
      where: { question }
    });
    return count > 0;
  } catch (error) {
    console.error('Failed to get questions by name in database');
    throw error;
  }
}

export async function getQuestionsCount(projectId) {
  try {
    return await db.questions.count({
      where: {
        projectId
      }
    });
  } catch (error) {
    console.error('Failed to get questions count in database');
    throw error;
  }
}

/**
 * 保存项目的问题列表
 * @param {string} projectId - 项目ID
 * @param {Array} questions - 问题列表
 * @param chunkId
 * @returns {Promise<Array>} - 保存后的问题列表
 */
export async function saveQuestions(projectId, questions, chunkId) {
  try {
    let data = questions.map(item => {
      return {
        projectId,
        chunkId: chunkId ? chunkId : item.chunkId,
        question: item.question,
        label: item.label
      };
    });
    return await db.questions.createMany({ data: data });
  } catch (error) {
    console.error('Failed to create questions in database');
    throw error;
  }
}

export async function updateQuestion(question) {
  try {
    return await db.questions.update({ where: { id: question.id }, data: question });
  } catch (error) {
    console.error('Failed to update questions in database');
    throw error;
  }
}

/**
 * 保存项目的问题列表（支持GA配对）
 * @param {string} projectId - 项目ID
 * @param {Array} questions - 问题列表
 * @param {string} chunkId - 文本块ID
 * @param {string} gaPairId - GA配对ID（可选）
 * @returns {Promise<Array>} - 保存后的问题列表
 */
export async function saveQuestionsWithGaPair(projectId, questions, chunkId, gaPairId = null) {
  try {
    let data = questions.map(item => {
      return {
        projectId,
        chunkId: chunkId ? chunkId : item.chunkId,
        question: item.question,
        label: item.label,
        gaPairId: gaPairId // 添加GA配对ID
      };
    });
    return await db.questions.createMany({ data: data });
  } catch (error) {
    console.error('Failed to create questions with GA pair in database');
    throw error;
  }
}

/**
 * 获取指定文本块的问题
 * @param {string} projectId - 项目ID
 * @param {string} chunkId - 文本块ID
 * @returns {Promise<Array>} - 问题列表
 */
export async function getQuestionsForChunk(projectId, chunkId) {
  return await db.questions.findMany({ where: { projectId, chunkId } });
}

/**
 * 删除单个问题
 * @param {string} questionId - 问题ID
 */
export async function deleteQuestion(questionId) {
  try {
    console.log(questionId);
    return await db.questions.delete({
      where: {
        id: questionId
      }
    });
  } catch (error) {
    console.error('Failed to delete questions by id in database');
    throw error;
  }
}

/**
 * 批量删除问题
 * @param {Array} questionIds
 */
export async function batchDeleteQuestions(questionIds) {
  try {
    return await db.questions.deleteMany({
      where: {
        id: {
          in: questionIds
        }
      }
    });
  } catch (error) {
    console.error('Failed to delete batch questions in database');
    throw error;
  }
}

'use client';

import { useState, useEffect } from 'react';
import {
  Box,
  Typography,
  Button,
  Card,
  CardContent,
  Switch,
  FormControlLabel,
  TextField,
  IconButton,
  Tooltip,
  Divider,
  Alert,
  CircularProgress,
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  Grid
} from '@mui/material';
import {
  Add as AddIcon,
  Delete as DeleteIcon,
  AutoFixHigh as AutoFixHighIcon,
  Save as SaveIcon
} from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import i18n from '@/lib/i18n';

/**
 * GA Pairs Manager Component
 * @param {Object} props
 * @param {string} props.projectId - Project ID
 * @param {string} props.fileId - File ID
 * @param {Function} props.onGaPairsChange - Callback when GA pairs change
 */
export default function GaPairsManager({ projectId, fileId, onGaPairsChange }) {
  const { t } = useTranslation();
  const [gaPairs, setGaPairs] = useState([]);
  const [backupGaPairs, setBackupGaPairs] = useState([]); // 备份状态
  const [loading, setLoading] = useState(false);
  const [generating, setGenerating] = useState(false);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(null);
  const [addDialogOpen, setAddDialogOpen] = useState(false);
  const [newGaPair, setNewGaPair] = useState({
    genreTitle: '',
    genreDesc: '',
    audienceTitle: '',
    audienceDesc: '',
    isActive: true
  });

  useEffect(() => {
    loadGaPairs();
  }, [projectId, fileId]);

  const loadGaPairs = async () => {
    try {
      setLoading(true);
      setError(null);

      const response = await fetch(`/api/projects/${projectId}/files/${fileId}/ga-pairs`);

      // 检查响应状态
      if (!response.ok) {
        if (response.status === 404) {
          console.warn('GA Pairs API not found, using empty data');
          setGaPairs([]);
          setBackupGaPairs([]);
          return;
        }
        throw new Error(`HTTP ${response.status}: Failed to load GA pairs`);
      }

      const result = await response.json();
      console.log('Load GA pairs result:', result);

      if (result.success) {
        const loadedData = result.data || [];
        setGaPairs(loadedData);
        setBackupGaPairs([...loadedData]); // 创建备份
        onGaPairsChange?.(loadedData);
      } else {
        throw new Error(result.error || 'Failed to load GA pairs');
      }
    } catch (error) {
      console.error('Load GA pairs error:', error);
      setError(t('gaPairs.loadError', { error: error.message }));
    } finally {
      setLoading(false);
    }
  };

  const generateGaPairs = async () => {
    try {
      setGenerating(true);
      setError(null);

      console.log('Starting GA pairs generation...');

      // Get current language from i18n
      const currentLanguage = i18n.language === 'en' ? 'en' : '中文';

      const response = await fetch(`/api/projects/${projectId}/files/${fileId}/ga-pairs`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          regenerate: false,
          appendMode: true, // 新增：启用追加模式
          language: currentLanguage
        })
      });

      if (!response.ok) {
        let errorMessage = t('gaPairs.generateError');

        if (response.status === 404) {
          errorMessage = t('gaPairs.serviceNotAvailable');
        } else if (response.status === 400) {
          try {
            const errorResult = await response.json();
            if (errorResult.error?.includes('No active AI model')) {
              errorMessage = t('gaPairs.noActiveModel');
            } else if (errorResult.error?.includes('content might be too short')) {
              errorMessage = t('gaPairs.contentTooShort');
            } else {
              errorMessage = errorResult.error || errorMessage;
            }
          } catch (parseError) {
            errorMessage = t('gaPairs.requestFailed', { status: response.status });
          }
        } else if (response.status === 500) {
          try {
            const errorResult = await response.json();
            if (errorResult.error?.includes('model configuration') || errorResult.error?.includes('Module not found')) {
              errorMessage = t('gaPairs.configError');
            } else {
              errorMessage = errorResult.error || 'Internal server error occurred.';
            }
          } catch (parseError) {
            console.error('Failed to parse error response:', parseError);
            errorMessage = errorResult.error || t('gaPairs.internalServerError');
          }
        }

        throw new Error(errorMessage);
      }

      // 处理成功响应
      const responseText = await response.text();
      if (!responseText || responseText.trim() === '') {
        throw new Error(t('gaPairs.emptyResponse'));
      }

      const result = JSON.parse(responseText);
      console.log('Generate GA pairs result:', result);

      if (result.success) {
        // 在追加模式下，后端只返回新生成的GA对
        const newGaPairs = result.data || [];

        // 将新生成的GA对追加到现有的GA对
        const updatedGaPairs = [...gaPairs, ...newGaPairs];

        setGaPairs(updatedGaPairs);
        setBackupGaPairs([...updatedGaPairs]); // 更新备份
        onGaPairsChange?.(updatedGaPairs);
        setSuccess(
          t('gaPairs.additionalPairsGenerated', {
            count: newGaPairs.length,
            total: updatedGaPairs.length
          })
        );
      } else {
        throw new Error(result.error || t('gaPairs.generationFailed'));
      }
    } catch (error) {
      console.error('Generate GA pairs error:', error);
      setError(error.message);
    } finally {
      setGenerating(false);
    }
  };

  const saveGaPairs = async () => {
    try {
      setSaving(true);
      setError(null);

      // 验证GA对数据
      const validatedGaPairs = gaPairs.map((pair, index) => {
        // 处理不同的数据格式
        let genreTitle, genreDesc, audienceTitle, audienceDesc;

        if (pair.genre && typeof pair.genre === 'object') {
          genreTitle = pair.genre.title;
          genreDesc = pair.genre.description;
        } else {
          genreTitle = pair.genreTitle || pair.genre;
          genreDesc = pair.genreDesc || '';
        }

        if (pair.audience && typeof pair.audience === 'object') {
          audienceTitle = pair.audience.title;
          audienceDesc = pair.audience.description;
        } else {
          audienceTitle = pair.audienceTitle || pair.audience;
          audienceDesc = pair.audienceDesc || '';
        }

        // 验证必填字段
        if (!genreTitle || !audienceTitle) {
          throw new Error(t('gaPairs.validationError', { number: index + 1 }));
        }

        return {
          id: pair.id,
          genreTitle: genreTitle.trim(),
          genreDesc: genreDesc.trim(),
          audienceTitle: audienceTitle.trim(),
          audienceDesc: audienceDesc.trim(),
          isActive: pair.isActive !== undefined ? pair.isActive : true
        };
      });

      console.log('Saving validated GA pairs:', validatedGaPairs);

      const response = await fetch(`/api/projects/${projectId}/files/${fileId}/ga-pairs`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          updates: validatedGaPairs
        })
      });

      if (!response.ok) {
        let errorMessage = t('gaPairs.saveError');

        if (response.status === 404) {
          errorMessage = 'GA Pairs save service is not available.';
        } else {
          try {
            const errorResult = await response.json();
            errorMessage = errorResult.error || errorMessage;
          } catch (parseError) {
            errorMessage = t('gaPairs.serverError', { status: response.status });
          }
        }

        throw new Error(errorMessage);
      }

      const responseText = await response.text();
      const result = responseText ? JSON.parse(responseText) : { success: true };

      if (result.success) {
        // 更新本地状态为服务器返回的数据
        const savedData = result.data || validatedGaPairs;
        setGaPairs(savedData);

        // 根据保存的GA对数量显示不同的成功消息
        if (savedData.length === 0) {
          setSuccess(t('gaPairs.allPairsDeleted'));
        } else {
          setSuccess(t('gaPairs.pairsSaved', { count: savedData.length }));
        }

        onGaPairsChange?.(savedData);
      } else {
        throw new Error(result.error || t('gaPairs.saveOperationFailed'));
      }
    } catch (error) {
      console.error('Save GA pairs error:', error);
      setError(error.message);
    } finally {
      setSaving(false);
    }
  };

  const handleGaPairChange = (index, field, value) => {
    const updatedGaPairs = [...gaPairs];

    // 确保对象存在
    if (!updatedGaPairs[index]) {
      console.error(`GA pair at index ${index} does not exist`);
      return;
    }

    updatedGaPairs[index] = {
      ...updatedGaPairs[index],
      [field]: value
    };

    setGaPairs(updatedGaPairs);
    // 不立即调用 onGaPairsChange，等用户点击保存时再调用
  };

  const handleDeleteGaPair = index => {
    const updatedGaPairs = gaPairs.filter((_, i) => i !== index);
    setGaPairs(updatedGaPairs);
    onGaPairsChange?.(updatedGaPairs);
  };

  const handleAddGaPair = () => {
    // 验证输入
    if (!newGaPair.genreTitle?.trim() || !newGaPair.audienceTitle?.trim()) {
      setError(t('gaPairs.requiredFields'));
      return;
    }

    // 创建新的GA对对象
    const newPair = {
      id: `temp_${Date.now()}`, // 临时ID
      genreTitle: newGaPair.genreTitle.trim(),
      genreDesc: newGaPair.genreDesc?.trim() || '',
      audienceTitle: newGaPair.audienceTitle.trim(),
      audienceDesc: newGaPair.audienceDesc?.trim() || '',
      isActive: true
    };

    const updatedGaPairs = [...gaPairs, newPair];
    setGaPairs(updatedGaPairs);
    onGaPairsChange?.(updatedGaPairs);

    // 重置表单并关闭对话框
    setNewGaPair({
      genreTitle: '',
      genreDesc: '',
      audienceTitle: '',
      audienceDesc: '',
      isActive: true
    });
    setAddDialogOpen(false);
    setError(null);
  };

  const resetMessages = () => {
    setError(null);
    setSuccess(null);
  };

  const recoverFromBackup = () => {
    setGaPairs([...backupGaPairs]);
    setError(null);
    setSuccess(t('gaPairs.restoredFromBackup'));
  };

  useEffect(() => {
    if (error || success) {
      const timer = setTimeout(resetMessages, 5000);
      return () => clearTimeout(timer);
    }
  }, [error, success]);

  if (loading) {
    return (
      <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', p: 4 }}>
        <CircularProgress />
        <Typography sx={{ ml: 2 }}>{t('gaPairs.loading')}</Typography>
      </Box>
    );
  }

  return (
    <Box sx={{ p: 2 }}>
      {/* Header with action buttons */}
      <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
        <Typography variant="h6">{t('gaPairs.title')}</Typography>
        <Box sx={{ display: 'flex', gap: 1 }}>
          {/* 右上角按钮为手动添加GA对 */}
          <Button
            variant="outlined"
            startIcon={<AddIcon />}
            onClick={() => setAddDialogOpen(true)}
            disabled={generating || saving}
          >
            {t('gaPairs.addPair')}
          </Button>
          <Button variant="contained" startIcon={<SaveIcon />} onClick={saveGaPairs} disabled={generating || saving}>
            {saving ? <CircularProgress size={20} /> : t('gaPairs.saveChanges')}
          </Button>
        </Box>
      </Box>

      {/* Error/Success Messages */}
      {error && (
        <Alert
          severity="error"
          sx={{ mb: 2 }}
          action={
            backupGaPairs.length > 0 && (
              <Button color="inherit" size="small" onClick={recoverFromBackup}>
                {t('gaPairs.restoreBackup')}
              </Button>
            )
          }
          onClose={resetMessages}
        >
          {error}
        </Alert>
      )}
      {success && (
        <Alert severity="success" sx={{ mb: 2 }} onClose={resetMessages}>
          {success}
        </Alert>
      )}

      {/* Generate GA Pairs Section - 只在没有GA对时显示 */}
      {gaPairs.length === 0 && (
        <Card sx={{ mb: 3 }}>
          <CardContent sx={{ textAlign: 'center' }}>
            <Typography variant="h6" gutterBottom>
              {t('gaPairs.noGaPairsTitle')}
            </Typography>
            <Typography color="textSecondary" sx={{ mb: 2 }}>
              {t('gaPairs.noGaPairsDescription')}
            </Typography>
            <Button
              variant="contained"
              startIcon={generating ? <CircularProgress size={20} /> : <AutoFixHighIcon />}
              onClick={generateGaPairs}
              disabled={generating}
              size="large"
            >
              {generating ? t('gaPairs.generating') : t('gaPairs.generateGaPairs')}
            </Button>
          </CardContent>
        </Card>
      )}

      {/* GA Pairs List */}
      {gaPairs.length > 0 && (
        <Box>
          <Typography variant="subtitle1" sx={{ mb: 2 }}>
            {t('gaPairs.activePairs', {
              active: gaPairs.filter(pair => pair.isActive).length,
              total: gaPairs.length
            })}
          </Typography>

          <Grid container spacing={2}>
            {gaPairs.map((pair, index) => (
              <Grid item xs={12} key={pair.id || index}>
                <Card variant="outlined">
                  <CardContent>
                    <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
                      <Typography variant="subtitle2" color="primary">
                        {t('gaPairs.pairNumber', { number: index + 1 })}
                      </Typography>
                      <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
                        <FormControlLabel
                          control={
                            <Switch
                              checked={pair.isActive}
                              onChange={e => handleGaPairChange(index, 'isActive', e.target.checked)}
                              size="small"
                            />
                          }
                          label={t('gaPairs.active')}
                        />
                        {/* 添加删除按钮 */}
                        <Tooltip title={t('gaPairs.deleteTooltip')}>
                          <IconButton size="small" color="error" onClick={() => handleDeleteGaPair(index)}>
                            <DeleteIcon fontSize="small" />
                          </IconButton>
                        </Tooltip>
                      </Box>
                    </Box>

                    <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
                      <TextField
                        label={t('gaPairs.genre')}
                        value={pair.genreTitle || pair.genre || ''}
                        onChange={e => handleGaPairChange(index, 'genreTitle', e.target.value)}
                        multiline
                        rows={2}
                        fullWidth
                        disabled={!pair.isActive}
                      />
                      <TextField
                        label={t('gaPairs.genreDescription')}
                        value={pair.genreDesc || ''}
                        onChange={e => handleGaPairChange(index, 'genreDesc', e.target.value)}
                        multiline
                        rows={2}
                        fullWidth
                        disabled={!pair.isActive}
                      />
                      <TextField
                        label={t('gaPairs.audience')}
                        value={pair.audienceTitle || pair.audience || ''}
                        onChange={e => handleGaPairChange(index, 'audienceTitle', e.target.value)}
                        multiline
                        rows={2}
                        fullWidth
                        disabled={!pair.isActive}
                      />
                      <TextField
                        label={t('gaPairs.audienceDescription')}
                        value={pair.audienceDesc || ''}
                        onChange={e => handleGaPairChange(index, 'audienceDesc', e.target.value)}
                        multiline
                        rows={2}
                        fullWidth
                        disabled={!pair.isActive}
                      />
                    </Box>
                  </CardContent>
                </Card>
              </Grid>
            ))}
          </Grid>

          {/* 在GA对列表下方添加生成按钮 */}
          <Box sx={{ mt: 3, textAlign: 'center' }}>
            <Button
              variant="outlined"
              startIcon={generating ? <CircularProgress size={20} /> : <AutoFixHighIcon />}
              onClick={generateGaPairs}
              disabled={generating}
            >
              {generating ? t('gaPairs.generating') : t('gaPairs.generateMore')}
            </Button>
          </Box>
        </Box>
      )}

      {/* Add GA Pair Dialog */}
      <Dialog open={addDialogOpen} onClose={() => setAddDialogOpen(false)} maxWidth="md" fullWidth>
        <DialogTitle>{t('gaPairs.addDialogTitle')}</DialogTitle>
        <DialogContent>
          <Box sx={{ pt: 1, display: 'flex', flexDirection: 'column', gap: 2 }}>
            <TextField
              label={t('gaPairs.genreTitle')}
              value={newGaPair.genreTitle || ''}
              onChange={e => setNewGaPair({ ...newGaPair, genreTitle: e.target.value })}
              fullWidth
              required
              placeholder={t('gaPairs.genreTitlePlaceholder')}
            />
            <TextField
              label={t('gaPairs.genreDescription')}
              value={newGaPair.genreDesc || ''}
              onChange={e => setNewGaPair({ ...newGaPair, genreDesc: e.target.value })}
              multiline
              rows={3}
              fullWidth
              placeholder={t('gaPairs.genreDescPlaceholder')}
            />
            <TextField
              label={t('gaPairs.audienceTitle')}
              value={newGaPair.audienceTitle || ''}
              onChange={e => setNewGaPair({ ...newGaPair, audienceTitle: e.target.value })}
              fullWidth
              required
              placeholder={t('gaPairs.audienceTitlePlaceholder')}
            />
            <TextField
              label={t('gaPairs.audienceDescription')}
              value={newGaPair.audienceDesc || ''}
              onChange={e => setNewGaPair({ ...newGaPair, audienceDesc: e.target.value })}
              multiline
              rows={3}
              fullWidth
              placeholder={t('gaPairs.audienceDescPlaceholder')}
            />
            <FormControlLabel
              control={
                <Switch
                  checked={newGaPair.isActive}
                  onChange={e => setNewGaPair({ ...newGaPair, isActive: e.target.checked })}
                />
              }
              label={t('gaPairs.active')}
            />
          </Box>
        </DialogContent>
        <DialogActions>
          <Button
            onClick={() => {
              setAddDialogOpen(false);
              // 重置表单
              setNewGaPair({
                genreTitle: '',
                genreDesc: '',
                audienceTitle: '',
                audienceDesc: '',
                isActive: true
              });
            }}
          >
            {t('gaPairs.cancel')}
          </Button>
          <Button
            onClick={handleAddGaPair}
            variant="contained"
            disabled={!newGaPair.genreTitle?.trim() || !newGaPair.audienceTitle?.trim()}
          >
            {t('gaPairs.addPairButton')}
          </Button>
        </DialogActions>
      </Dialog>
    </Box>
  );
}

// 获取项目详情
import { deleteProject, getProject, updateProject, getTaskConfig } from '@/lib/db/projects';

export async function GET(request, { params }) {
  try {
    const { projectId } = params;
    const project = await getProject(projectId);
    const taskConfig = await getTaskConfig(projectId);
    if (!project) {
      return Response.json({ error: '项目不存在' }, { status: 404 });
    }
    return Response.json({ ...project, taskConfig });
  } catch (error) {
    console.error('获取项目详情出错:', String(error));
    return Response.json({ error: String(error) }, { status: 500 });
  }
}

// 更新项目
export async function PUT(request, { params }) {
  try {
    const { projectId } = params;
    const projectData = await request.json();

    // 验证必要的字段
    if (!projectData.name && !projectData.defaultModelConfigId) {
      return Response.json({ error: '项目名称不能为空' }, { status: 400 });
    }

    const updatedProject = await updateProject(projectId, projectData);

    if (!updatedProject) {
      return Response.json({ error: '项目不存在' }, { status: 404 });
    }

    return Response.json(updatedProject);
  } catch (error) {
    console.error('更新项目出错:', String(error));
    return Response.json({ error: String(error) }, { status: 500 });
  }
}

// 删除项目
export async function DELETE(request, { params }) {
  try {
    const { projectId } = params;
    const success = await deleteProject(projectId);

    if (!success) {
      return Response.json({ error: '项目不存在' }, { status: 404 });
    }

    return Response.json({ success: true });
  } catch (error) {
    console.error('删除项目出错:', error);
    return Response.json({ error: error.message }, { status: 500 });
  }
}

'use client';

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAtomValue } from 'jotai/index';
import { selectedModelInfoAtom } from '@/lib/store';
import axios from 'axios';
import { toast } from 'sonner';
import i18n from '@/lib/i18n';

/**
 * 数据集详情页面业务逻辑 Hook
 */
export default function useDatasetDetails(projectId, datasetId) {
  const router = useRouter();
  const [datasets, setDatasets] = useState([]);
  const [currentDataset, setCurrentDataset] = useState(null);
  const [loading, setLoading] = useState(true);
  const [editingAnswer, setEditingAnswer] = useState(false);
  const [editingCot, setEditingCot] = useState(false);
  const [answerValue, setAnswerValue] = useState('');
  const [cotValue, setCotValue] = useState('');
  const [snackbar, setSnackbar] = useState({
    open: false,
    message: '',
    severity: 'success'
  });
  const [confirming, setConfirming] = useState(false);
  const [optimizeDialog, setOptimizeDialog] = useState({
    open: false,
    loading: false
  });
  const [viewDialogOpen, setViewDialogOpen] = useState(false);
  const [viewChunk, setViewChunk] = useState(null);
  const [datasetsAllCount, setDatasetsAllCount] = useState(0);
  const [datasetsConfirmCount, setDatasetsConfirmCount] = useState(0);
  const [answerTokens, setAnswerTokens] = useState(0);
  const [cotTokens, setCotTokens] = useState(0);
  const model = useAtomValue(selectedModelInfoAtom);
  const [shortcutsEnabled, setShortcutsEnabled] = useState(() => {
    const storedValue = localStorage.getItem('shortcutsEnabled');
    return storedValue !== null ? storedValue === 'true' : false;
  });

  // 异步获取Token数量
  const fetchTokenCount = async () => {
    try {
      const response = await fetch(`/api/projects/${projectId}/datasets/${datasetId}/token-count`);
      if (response.ok) {
        const data = await response.json();
        if (data.answerTokens !== undefined) {
          setAnswerTokens(data.answerTokens);
        }
        if (data.cotTokens !== undefined) {
          setCotTokens(data.cotTokens);
        }
      }
    } catch (error) {
      console.error('获取Token数量失败:', error);
      // Token加载失败不阻塞主界面或显示错误提示
    }
  };

  // 获取数据集详情
  const fetchDatasets = async () => {
    try {
      const response = await fetch(`/api/projects/${projectId}/datasets/${datasetId}`);
      if (!response.ok) throw new Error('获取数据集详情失败');
      const data = await response.json();
      setCurrentDataset(data.datasets);
      setCotValue(data.datasets?.cot);
      setAnswerValue(data.datasets?.answer);
      setDatasetsAllCount(data.total);
      setDatasetsConfirmCount(data.confirmedCount);

      // 数据加载完成后，异步获取Token数量
      fetchTokenCount();
    } catch (error) {
      setSnackbar({
        open: true,
        message: error.message,
        severity: 'error'
      });
    } finally {
      setLoading(false);
    }
  };

  // 确认并保存数据集
  const handleConfirm = async () => {
    try {
      setConfirming(true);
      const response = await fetch(`/api/projects/${projectId}/datasets?id=${datasetId}`, {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          confirmed: true
        })
      });

      if (!response.ok) {
        throw new Error('操作失败');
      }

      setCurrentDataset(prev => ({ ...prev, confirmed: true }));

      setSnackbar({
        open: true,
        message: '操作成功',
        severity: 'success'
      });

      // 导航到下一个数据集
      handleNavigate('next');
    } catch (error) {
      setSnackbar({
        open: true,
        message: error.message || '操作失败',
        severity: 'error'
      });
    } finally {
      setConfirming(false);
    }
  };

  // 导航到其他数据集
  const handleNavigate = async direction => {
    const response = await axios.get(`/api/projects/${projectId}/datasets/${datasetId}?operateType=${direction}`);
    if (response.data) {
      router.push(`/projects/${projectId}/datasets/${response.data.id}`);
    } else {
      toast.warning(`已经是${direction === 'next' ? '最后' : '第'}一条数据了`);
    }
  };

  // 保存编辑
  const handleSave = async (field, value) => {
    try {
      const response = await fetch(`/api/projects/${projectId}/datasets?id=${datasetId}`, {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          [field]: value
        })
      });

      if (!response.ok) {
        throw new Error('保存失败');
      }

      const data = await response.json();
      setCurrentDataset(prev => ({ ...prev, [field]: value }));

      setSnackbar({
        open: true,
        message: '保存成功',
        severity: 'success'
      });

      // 重置编辑状态
      if (field === 'answer') setEditingAnswer(false);
      if (field === 'cot') setEditingCot(false);
    } catch (error) {
      setSnackbar({
        open: true,
        message: error.message || '保存失败',
        severity: 'error'
      });
    }
  };

  // 删除数据集
  const handleDelete = async () => {
    if (!confirm('确定要删除这条数据吗？此操作不可撤销。')) return;

    try {
      // 尝试获取下一个数据集，在删除前先确保有可导航的目标
      const nextResponse = await axios.get(`/api/projects/${projectId}/datasets/${datasetId}?operateType=next`);
      const hasNextDataset = !!nextResponse.data;
      const nextDatasetId = hasNextDataset ? nextResponse.data.id : null;

      // 删除当前数据集
      const deleteResponse = await fetch(`/api/projects/${projectId}/datasets?id=${datasetId}`, {
        method: 'DELETE'
      });

      if (!deleteResponse.ok) {
        throw new Error('删除失败');
      }

      // 导航逻辑：有下一个就跳转下一个，没有则返回列表页
      if (hasNextDataset) {
        router.push(`/projects/${projectId}/datasets/${nextDatasetId}`);
      } else {
        // 没有更多数据集，返回列表页面
        router.push(`/projects/${projectId}/datasets`);
      }

      toast.success('删除成功');
    } catch (error) {
      setSnackbar({
        open: true,
        message: error.message || '删除失败',
        severity: 'error'
      });
    }
  };

  // 优化对话框相关操作
  const handleOpenOptimizeDialog = () => {
    setOptimizeDialog({
      open: true,
      loading: false
    });
  };

  const handleCloseOptimizeDialog = () => {
    if (optimizeDialog.loading) return;
    setOptimizeDialog({
      open: false,
      loading: false
    });
  };

  // 优化操作
  const handleOptimize = async advice => {
    if (!model) {
      setSnackbar({
        open: true,
        message: '请先选择模型，可以在顶部导航栏选择',
        severity: 'error'
      });
      setOptimizeDialog(prev => ({ ...prev, open: false }));
      return;
    }

    try {
      setOptimizeDialog(prev => ({ ...prev, loading: true }));
      const language = i18n.language === 'zh-CN' ? '中文' : 'en';
      const response = await fetch(`/api/projects/${projectId}/datasets/optimize`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          datasetId,
          model,
          advice,
          language
        })
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.error || '优化失败');
      }

      // 优化成功后，重新查询数据以获取最新状态
      await fetchDatasets();
      // 优化可能改变了文本内容，重新获取Token计数
      fetchTokenCount();

      setSnackbar({
        open: true,
        message: 'AI智能优化成功',
        severity: 'success'
      });
    } catch (error) {
      setSnackbar({
        open: true,
        message: error.message || '优化失败',
        severity: 'error'
      });
    } finally {
      setOptimizeDialog({
        open: false,
        loading: false
      });
    }
  };

  // 查看文本块详情
  const handleViewChunk = async chunkContent => {
    try {
      setViewChunk(chunkContent);
      setViewDialogOpen(true);
    } catch (error) {
      console.error('查看文本块出错', error);
      setSnackbar({
        open: true,
        message: error.message,
        severity: 'error'
      });
      setViewDialogOpen(false);
    }
  };

  // 关闭文本块详情对话框
  const handleCloseViewDialog = () => {
    setViewDialogOpen(false);
  };

  // 初始化和快捷键事件
  useEffect(() => {
    fetchDatasets();
  }, [projectId, datasetId]);

  // 快捷键状态变化
  useEffect(() => {
    localStorage.setItem('shortcutsEnabled', shortcutsEnabled);
  }, [shortcutsEnabled]);

  // 监听键盘事件
  useEffect(() => {
    const handleKeyDown = event => {
      if (!shortcutsEnabled) return;
      switch (event.key) {
        case 'ArrowLeft': // 上一个
          handleNavigate('prev');
          break;
        case 'ArrowRight': // 下一个
          handleNavigate('next');
          break;
        case 'y': // 确认
        case 'Y':
          if (!confirming && currentDataset && !currentDataset.confirmed) {
            handleConfirm();
          }
          break;
        case 'd': // 删除
        case 'D':
          handleDelete();
          break;
        default:
          break;
      }
    };
    window.addEventListener('keydown', handleKeyDown);
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [shortcutsEnabled, confirming, currentDataset]);

  return {
    loading,
    currentDataset,
    answerValue,
    cotValue,
    editingAnswer,
    editingCot,
    confirming,
    snackbar,
    optimizeDialog,
    viewDialogOpen,
    viewChunk,
    datasetsAllCount,
    datasetsConfirmCount,
    answerTokens,
    cotTokens,
    shortcutsEnabled,
    setShortcutsEnabled,
    setSnackbar,
    setAnswerValue,
    setCotValue,
    setEditingAnswer,
    setEditingCot,
    handleNavigate,
    handleConfirm,
    handleSave,
    handleDelete,
    handleOpenOptimizeDialog,
    handleCloseOptimizeDialog,
    handleOptimize,
    handleViewChunk,
    handleCloseViewDialog
  };
}

import { NextResponse } from 'next/server';
import { createInitModelConfig, getModelConfigByProjectId, saveModelConfig } from '@/lib/db/model-config';
import { DEFAULT_MODEL_SETTINGS, MODEL_PROVIDERS } from '@/constant/model';
import { getProject } from '@/lib/db/projects';

// 获取模型配置列表
export async function GET(request, { params }) {
  try {
    const { projectId } = params;
    // 验证项目 ID
    if (!projectId) {
      return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
    }
    let modelConfigList = await getModelConfigByProjectId(projectId);
    if (!modelConfigList || modelConfigList.length === 0) {
      let insertModelConfigList = [];
      MODEL_PROVIDERS.forEach(item => {
        let data = {
          projectId: projectId,
          providerId: item.id,
          providerName: item.name,
          endpoint: item.defaultEndpoint,
          apiKey: '',
          modelId: item.defaultModels.length > 0 ? item.defaultModels[0] : '',
          modelName: item.defaultModels.length > 0 ? item.defaultModels[0] : '',
          type: 'text',
          temperature: DEFAULT_MODEL_SETTINGS.temperature,
          maxTokens: DEFAULT_MODEL_SETTINGS.maxTokens,
          topK: 0,
          topP: 0,
          status: 1
        };
        insertModelConfigList.push(data);
      });
      modelConfigList = await createInitModelConfig(insertModelConfigList);
    }
    let project = await getProject(projectId);
    return NextResponse.json({ data: modelConfigList, defaultModelConfigId: project.defaultModelConfigId });
  } catch (error) {
    console.error('Error obtaining model configuration:', String(error));
    return NextResponse.json({ error: 'Failed to obtain model configuration' }, { status: 500 });
  }
}

// 保存模型配置
export async function POST(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目 ID
    if (!projectId) {
      return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
    }
    // 获取请求体
    const modelConfig = await request.json();

    // 验证请求体
    if (!modelConfig) {
      return NextResponse.json({ error: 'The model configuration cannot be empty ' }, { status: 400 });
    }
    modelConfig.projectId = projectId;
    if (!modelConfig.modelId) {
      modelConfig.modelId = modelConfig.modelName;
    }
    const res = await saveModelConfig(modelConfig);

    return NextResponse.json(res);
  } catch (error) {
    console.error('Error updating model configuration:', String(error));
    return NextResponse.json({ error: 'Failed to update model configuration' }, { status: 500 });
  }
}

'use client';

import { useState, useEffect } from 'react';
import { useAtomValue } from 'jotai/index';
import { modelConfigListAtom } from '@/lib/store';

export default function useModelPlayground(projectId, defaultModelId = null) {
  // 状态管理
  const [selectedModels, setSelectedModels] = useState(defaultModelId ? [defaultModelId] : []);
  const [loading, setLoading] = useState({});
  const [userInput, setUserInput] = useState('');
  const [conversations, setConversations] = useState({});
  const [error, setError] = useState(null);
  const [outputMode, setOutputMode] = useState('normal'); // 'normal' 或 'streaming'
  const [uploadedImage, setUploadedImage] = useState(null); // 存储上传的图片Base64

  const availableModels = useAtomValue(modelConfigListAtom);

  // 初始化会话状态
  useEffect(() => {
    if (selectedModels.length > 0) {
      const initialConversations = {};
      selectedModels.forEach(modelId => {
        if (!conversations[modelId]) {
          initialConversations[modelId] = [];
        }
      });

      if (Object.keys(initialConversations).length > 0) {
        setConversations(prev => ({
          ...prev,
          ...initialConversations
        }));
      }
    }
  }, [selectedModels]);

  // 处理模型选择
  const handleModelSelection = event => {
    const {
      target: { value }
    } = event;

    // 限制最多选择 3 个模型
    const selectedValues = typeof value === 'string' ? value.split(',') : value;
    const limitedSelection = selectedValues.slice(0, 3);

    setSelectedModels(limitedSelection);
  };

  // 处理用户输入
  const handleInputChange = e => {
    setUserInput(e.target.value);
  };

  // 处理图片上传
  const handleImageUpload = e => {
    const file = e.target.files[0];
    if (file) {
      const reader = new FileReader();
      reader.onloadend = () => {
        setUploadedImage(reader.result);
      };
      reader.readAsDataURL(file);
    }
  };

  // 删除已上传的图片
  const handleRemoveImage = () => {
    setUploadedImage(null);
  };

  // 处理输出模式切换
  const handleOutputModeChange = event => {
    setOutputMode(event.target.value);
  };

  // 发送消息给所有选中的模型
  const handleSendMessage = async () => {
    if (!userInput.trim() || Object.values(loading).some(value => value) || selectedModels.length === 0) return;

    // 获取用户输入
    const input = userInput.trim();
    setUserInput('');

    // 获取图片（如果有的话）
    const image = uploadedImage;
    setUploadedImage(null); // 清除图片

    // 更新所有选中模型的对话
    const updatedConversations = { ...conversations };
    selectedModels.forEach(modelId => {
      if (!updatedConversations[modelId]) {
        updatedConversations[modelId] = [];
      }
      // 检查是否有图片并且当前模型是视觉模型
      const model = availableModels.find(m => m.id === modelId);
      const isVisionModel = model && model.type === 'vision';

      if (isVisionModel && image) {
        // 如果是视觉模型并且有图片，使用复合格式
        updatedConversations[modelId].push({
          role: 'user',
          content: [
            { type: 'text', text: input || '请描述这个图片' },
            { type: 'image_url', image_url: { url: image } }
          ]
        });
      } else {
        // 其他情况使用纯文本
        updatedConversations[modelId].push({
          role: 'user',
          content: input
        });
      }
    });

    setConversations(updatedConversations);

    // 为每个模型设置独立的加载状态
    const updatedLoading = {};
    selectedModels.forEach(modelId => {
      updatedLoading[modelId] = true;
    });
    setLoading(updatedLoading);

    // 为每个模型单独发送请求
    selectedModels.forEach(async modelId => {
      const model = availableModels.find(m => m.id === modelId);
      if (!model) {
        // 模型配置不存在
        const modelConversation = [...(updatedConversations[modelId] || [])];

        // 更新对话状态
        setConversations(prev => ({
          ...prev,
          [modelId]: [...modelConversation, { role: 'error', content: '模型配置不存在' }]
        }));

        // 更新加载状态
        setLoading(prev => ({ ...prev, [modelId]: false }));
        return;
      }

      try {
        // 检查是否是视觉模型且有图片
        const isVisionModel = model.type === 'vision';

        // 构建请求消息
        let requestMessages = [...updatedConversations[modelId]]; // 复制当前消息历史

        // 如果是vision模型并且有图片，将最后一条用户消息替换为包含图片的消息
        if (isVisionModel && image && requestMessages.length > 0) {
          // 找到最后一条用户消息
          const lastUserMsgIndex = requestMessages.length - 1;
          // 替换为包含图片的消息
          requestMessages[lastUserMsgIndex] = {
            role: 'user',
            content: [
              { type: 'text', text: input || '请描述这个图片' },
              { type: 'image_url', image_url: { url: image } }
            ]
          };
        }

        // 根据输出模式选择不同的处理方式
        if (outputMode === 'streaming') {
          // 流式输出处理
          // 先添加一个空的助手回复，用于后续流式更新
          setConversations(prev => {
            const modelConversation = [...(prev[modelId] || [])];
            return {
              ...prev,
              [modelId]: [
                ...modelConversation,
                {
                  role: 'assistant',
                  content: '',
                  isStreaming: true,
                  thinking: '', // 添加推理过程字段
                  showThinking: true // 默认显示推理过程
                }
              ]
            };
          });

          const response = await fetch(`/api/projects/${projectId}/playground/chat/stream`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              model: model,
              messages: requestMessages
            })
          });

          if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
          }

          const reader = response.body.getReader();
          const decoder = new TextDecoder('utf-8');
          let accumulatedContent = '';

          // 状态变量，用于跟踪是否正在处理思维链
          let isInThinking = false;
          let currentThinking = '';
          let currentContent = '';

          while (true) {
            const { done, value } = await reader.read();
            if (done) break;

            // 解码收到的数据块
            const chunk = decoder.decode(value, { stream: true });

            // 处理当前数据块
            for (let i = 0; i < chunk.length; i++) {
              const char = chunk[i];

              // 检测开始标签 <think>
              if (i + 6 <= chunk.length && chunk.substring(i, i + 7) === '<think>') {
                isInThinking = true;
                i += 6; // 跳过标签
                continue;
              }

              // 检测结束标签 </think>
              if (i + 7 <= chunk.length && chunk.substring(i, i + 8) === '</think>') {
                isInThinking = false;
                i += 7; // 跳过标签
                continue;
              }

              // 根据当前状态添加到对应内容中
              if (isInThinking) {
                currentThinking += char;
              } else {
                currentContent += char;
              }
            }

            // 累积全部内容以便最终处理
            accumulatedContent += chunk;

            // 更新对话内容
            setConversations(prev => {
              const modelConversation = [...prev[modelId]];
              const lastIndex = modelConversation.length - 1;

              // 更新最后一条消息的内容，包括思维链
              modelConversation[lastIndex] = {
                ...modelConversation[lastIndex],
                content: currentContent,
                thinking: currentThinking,
                showThinking: currentThinking.length > 0 // 只要有思维链内容就显示
              };

              return {
                ...prev,
                [modelId]: modelConversation
              };
            });
          }

          // 完成流式传输，移除流式标记
          // 使用刚刚实时跟踪的 currentThinking 和 currentContent作为最终的思维链和内容
          let finalThinking = currentThinking;
          let finalAnswer = currentContent;

          // 如果到流结束时还在思维链中，确保解析完整的思维链内容
          if (isInThinking) {
            console.log('警告: 流结束时仍在思维链中，可能有标签不完整');
            isInThinking = false;
          }

          setConversations(prev => {
            const modelConversation = [...prev[modelId]];
            const lastIndex = modelConversation.length - 1;

            // 更新最后一条消息，移除流式标记
            modelConversation[lastIndex] = {
              role: 'assistant',
              content: finalAnswer,
              thinking: finalThinking,
              showThinking: finalThinking ? true : false,
              isStreaming: false
            };

            return {
              ...prev,
              [modelId]: modelConversation
            };
          });
        } else {
          // 普通输出处理
          const response = await fetch(`/api/projects/${projectId}/playground/chat`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              model: {
                ...model,
                extra_body: { enable_thinking: true } // 启用思考链
              },
              messages: requestMessages
            })
          });

          // 获取响应数据
          const data = await response.json();

          // 独立更新此模型的对话状态
          setConversations(prev => {
            const modelConversation = [...(prev[modelId] || [])];

            if (response.ok) {
              // 处理可能包含思考链的内容
              let thinking = '';
              let content = data.response;

              // 检查是否包含思考链
              if (content && content.includes('<think>')) {
                const thinkParts = content.split(/<think>(.*?)<\/think>/s);
                if (thinkParts.length >= 3) {
                  thinking = thinkParts[1] || '';
                  // 移除思考链部分，只保留最终回答
                  content = thinkParts.filter((_, i) => i % 2 === 0).join('');
                }
              }

              return {
                ...prev,
                [modelId]: [
                  ...modelConversation,
                  {
                    role: 'assistant',
                    content: content,
                    thinking: thinking,
                    showThinking: thinking ? true : false
                  }
                ]
              };
            } else {
              return {
                ...prev,
                [modelId]: [...modelConversation, { role: 'error', content: `错误: ${data.error || '请求失败'}` }]
              };
            }
          });
        }
      } catch (error) {
        console.error(`请求模型 ${model.name} 失败:`, error);

        // 独立更新此模型的对话状态 - 添加错误消息
        setConversations(prev => {
          const modelConversation = [...(prev[modelId] || [])];
          return {
            ...prev,
            [modelId]: [...modelConversation, { role: 'error', content: `错误: ${error.message}` }]
          };
        });
      } finally {
        // 更新此模型的加载状态
        setLoading(prev => ({ ...prev, [modelId]: false }));
      }
    });
  };

  // 清空所有对话
  const handleClearConversations = () => {
    const clearedConversations = {};
    selectedModels.forEach(modelId => {
      clearedConversations[modelId] = [];
    });
    setConversations(clearedConversations);
    setLoading({});
  };

  // 获取模型名称
  const getModelName = modelId => {
    const model = availableModels.find(m => m.id === modelId);
    return model ? `${model.provider}: ${model.name}` : modelId;
  };

  return {
    availableModels,
    selectedModels,
    loading,
    userInput,
    conversations,
    error,
    outputMode,
    uploadedImage,
    handleModelSelection,
    handleInputChange,
    handleImageUpload,
    handleRemoveImage,
    handleSendMessage,
    handleClearConversations,
    handleOutputModeChange,
    getModelName
  };
}

import { useCallback } from 'react';
import { toast } from 'sonner';
import i18n from '@/lib/i18n';
import axios from 'axios';
import { useAtomValue } from 'jotai/index';
import { selectedModelInfoAtom } from '@/lib/store';
import { useTranslation } from 'react-i18next';

export function useGenerateDataset() {
  const model = useAtomValue(selectedModelInfoAtom);
  const { t } = useTranslation();

  const generateSingleDataset = useCallback(
    async ({ projectId, questionId, questionInfo }) => {
      // 获取模型参数
      if (!model) {
        toast.error(t('models.configNotFound'));
        return null;
      }
      // 调用API生成数据集
      const currentLanguage = i18n.language === 'zh-CN' ? '中文' : 'en';
      toast.promise(
        axios.post(`/api/projects/${projectId}/datasets`, {
          questionId,
          model,
          language: currentLanguage
        }),
        {
          loading: t('datasets.generating'),
          description: `问题：【${questionInfo}】`,
          position: 'top-right',
          success: data => {
            return '生成数据集成功';
          },
          error: error => {
            return t('datasets.generateFailed', { error: error.response?.data?.error });
          }
        }
      );
    },
    [model, t]
  );

  const generateMultipleDataset = useCallback(
    async (projectId, questions) => {
      let completed = 0;
      const total = questions.length;
      // 显示带进度的Loading
      const loadingToastId = toast.loading(`正在处理请求 (${completed}/${total})...`, { position: 'top-right' });

      // 处理每个请求
      const processRequest = async question => {
        try {
          const response = await axios.post(`/api/projects/${projectId}/datasets`, {
            questionId: question.id,
            model,
            language: i18n.language === 'zh-CN' ? '中文' : 'en'
          });
          const data = response.data;
          completed++;
          toast.success(`${question.question} 完成`, { position: 'top-right' });
          toast.loading(`正在处理请求 (${completed}/${total})...`, { id: loadingToastId });
          return data;
        } catch (error) {
          completed++;
          toast.error(`${question.question} 失败`, {
            description: error.message,
            position: 'top-right'
          });
          toast.loading(`正在处理请求 (${completed}/${total})...`, { id: loadingToastId });
          throw error;
        }
      };

      try {
        const results = await Promise.allSettled(questions.map(req => processRequest(req)));
        // 全部完成后更新Loading为完成状态
        toast.success(`全部请求处理完成 (成功: ${results.filter(r => r.status === 'fulfilled').length}/${total})`, {
          id: loadingToastId,
          position: 'top-right'
        });
        return results;
      } catch {
        // Promise.allSettled不会进入catch，这里只是保险
      }
    },
    [model, t]
  );

  return { generateSingleDataset, generateMultipleDataset };
}

import { NextResponse } from 'next/server';
import { getProjectRoot } from '@/lib/db/base';
import path from 'path';
import fs from 'fs/promises';

export async function GET(request, { params }) {
  try {
    const { projectId, modelId } = params;

    // 验证项目ID和模型ID
    if (!projectId || !modelId) {
      return NextResponse.json({ error: 'The project ID and model ID cannot be empty' }, { status: 400 });
    }

    // 获取项目根目录
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);

    // 检查项目是否存在
    try {
      await fs.access(projectPath);
    } catch (error) {
      return NextResponse.json({ error: 'The project does not exist' }, { status: 404 });
    }

    // 获取模型配置文件路径
    const modelConfigPath = path.join(projectPath, 'model-config.json');

    // 检查模型配置文件是否存在
    try {
      await fs.access(modelConfigPath);
    } catch (error) {
      return NextResponse.json({ error: 'The model configuration does not exist' }, { status: 404 });
    }

    // 读取模型配置文件
    const modelConfigData = await fs.readFile(modelConfigPath, 'utf-8');
    const modelConfig = JSON.parse(modelConfigData);

    // 查找指定ID的模型
    const model = modelConfig.find(model => model.id === modelId);

    if (!model) {
      return NextResponse.json({ error: 'The model does not exist' }, { status: 404 });
    }

    return NextResponse.json(model);
  } catch (error) {
    console.error('Error getting model:', String(error));
    return NextResponse.json({ error: 'Failed to get model' }, { status: 500 });
  }
}

export async function PUT(request, { params }) {
  try {
    const { projectId, modelId } = params;

    // 验证项目ID和模型ID
    if (!projectId || !modelId) {
      return NextResponse.json({ error: 'The project ID and model ID cannot be empty' }, { status: 400 });
    }

    // 获取请求体
    const modelData = await request.json();

    // 验证请求体
    if (!modelData || !modelData.provider || !modelData.name) {
      return NextResponse.json({ error: 'The model data is incomplete' }, { status: 400 });
    }

    // 获取项目根目录
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);

    // 检查项目是否存在
    try {
      await fs.access(projectPath);
    } catch (error) {
      return NextResponse.json({ error: 'The project does not exist' }, { status: 404 });
    }

    // 获取模型配置文件路径
    const modelConfigPath = path.join(projectPath, 'model-config.json');

    // 读取模型配置文件
    let modelConfig = [];
    try {
      const modelConfigData = await fs.readFile(modelConfigPath, 'utf-8');
      modelConfig = JSON.parse(modelConfigData);
    } catch (error) {
      // 如果文件不存在，创建一个空数组
    }

    // 更新模型数据
    const modelIndex = modelConfig.findIndex(model => model.id === modelId);

    if (modelIndex >= 0) {
      // 更新现有模型
      modelConfig[modelIndex] = {
        ...modelConfig[modelIndex],
        ...modelData,
        id: modelId // 确保ID不变
      };
    } else {
      // 添加新模型
      modelConfig.push({
        ...modelData,
        id: modelId
      });
    }

    // 写入模型配置文件
    await fs.writeFile(modelConfigPath, JSON.stringify(modelConfig, null, 2), 'utf-8');

    return NextResponse.json({ message: 'Model configuration updated successfully' });
  } catch (error) {
    console.error('Error updating model configuration:', String(error));
    return NextResponse.json({ error: 'Failed to update model configuration' }, { status: 500 });
  }
}

export async function DELETE(request, { params }) {
  try {
    const { projectId, modelId } = params;

    // 验证项目ID和模型ID
    if (!projectId || !modelId) {
      return NextResponse.json({ error: 'The project ID and model ID cannot be empty' }, { status: 400 });
    }

    // 获取项目根目录
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);

    // 检查项目是否存在
    try {
      await fs.access(projectPath);
    } catch (error) {
      return NextResponse.json({ error: 'The project does not exist' }, { status: 404 });
    }

    // 获取模型配置文件路径
    const modelConfigPath = path.join(projectPath, 'model-config.json');

    // 检查模型配置文件是否存在
    try {
      await fs.access(modelConfigPath);
    } catch (error) {
      return NextResponse.json({ error: 'The model configuration does not exist' }, { status: 404 });
    }

    // 读取模型配置文件
    const modelConfigData = await fs.readFile(modelConfigPath, 'utf-8');
    let modelConfig = JSON.parse(modelConfigData);

    // 过滤掉要删除的模型
    const initialLength = modelConfig.length;
    modelConfig = modelConfig.filter(model => model.id !== modelId);

    // 检查是否找到并删除了模型
    if (modelConfig.length === initialLength) {
      return NextResponse.json({ error: 'The model does not exist' }, { status: 404 });
    }

    // 写入模型配置文件
    await fs.writeFile(modelConfigPath, JSON.stringify(modelConfig, null, 2), 'utf-8');

    return NextResponse.json({ message: 'Model deleted successfully' });
  } catch (error) {
    console.error('Error deleting model:', String(error));
    return NextResponse.json({ error: 'Failed to delete model' }, { status: 500 });
  }
}

import { getActiveModel } from '@/lib/services/models';
import logger from '@/lib/util/logger';
import { GA_GENERATION_PROMPT } from '@/lib/llm/prompts/ga-generation';
import { GA_GENERATION_PROMPT_EN } from '@/lib/llm/prompts/ga-generationEn';
import { extractJsonFromLLMOutput } from '@/lib/llm/common/util';
const LLMClient = require('@/lib/llm/core');

/**
 * Generate GA pairs for text content using LLM
 * @param {string} textContent - The text content to analyze
 * @param {string} projectId - The project ID to get the active model for
 * @param {string} language - Language for generation (default: '中文')
 * @returns {Promise<Array>} - Generated GA pairs
 */
export async function generateGaPairs(textContent, projectId, language = '中文') {
  try {
    logger.info('Starting GA pairs generation');

    // 验证输入参数
    if (!textContent || typeof textContent !== 'string') {
      throw new Error('Invalid text content provided');
    }

    if (!projectId) {
      throw new Error('Project ID is required');
    }

    // Get model configuration
    const model = await getActiveModel(projectId);
    if (!model) {
      throw new Error('No active model available for GA generation');
    }

    logger.info(`Using model: ${model.modelName} for project ${projectId}`);

    const promptTemplate = language === 'en' ? GA_GENERATION_PROMPT_EN : GA_GENERATION_PROMPT;

    // Prepare the prompt with text content
    const prompt = promptTemplate.replace('{text_content}', textContent.slice(0, 10000));

    if (!prompt) {
      throw new Error('Failed to generate prompt');
    }

    // Call the LLM API
    const response = await callLLMAPI(model, prompt);

    if (!response) {
      throw new Error('Empty response from LLM');
    }

    // Parse the response
    const gaPairs = parseGaResponse(response);

    logger.info(`Successfully generated ${gaPairs.length} GA pairs`);
    return gaPairs;
  } catch (error) {
    logger.error('Failed to generate GA pairs:', error);
    throw error;
  }
}

/**
 * Call LLM API with the given model and prompt
 * @param {Object} model - Model configuration
 * @param {string} prompt - The prompt to send
 * @returns {Promise<Object|Array>} - Parsed JSON object/array
 */
async function callLLMAPI(model, prompt) {
  try {
    if (!model || !prompt) {
      throw new Error('Model and prompt are required');
    }

    logger.info('Calling LLM API...');

    const llmClient = new LLMClient(model);
    const response = await llmClient.getResponse(prompt); // Changed from llmClient.chat

    if (!response) {
      throw new Error('Invalid response from LLM');
    }

    return response;
  } catch (error) {
    logger.error('LLM API call failed:', error);
    throw new Error(`LLM API call failed: ${error.message}`);
  }
}

/**
 * Parse GA pairs from LLM response
 * @param {string} response - Raw LLM response
 * @returns {Array} - Parsed GA pairs
 */
function parseGaResponse(response) {
  try {
    // Log the raw response for debugging
    logger.info('Raw LLM response length:', response.length);

    const parsed = extractJsonFromLLMOutput(response);

    if (!parsed) {
      throw new Error('Failed to extract JSON from LLM response');
    }

    // Handle case where response is wrapped in an object
    let gaPairsArray = parsed;
    if (!Array.isArray(parsed)) {
      // Check if it's wrapped in a property
      if (parsed.gaPairs && Array.isArray(parsed.gaPairs)) {
        gaPairsArray = parsed.gaPairs;
      } else if (parsed.pairs && Array.isArray(parsed.pairs)) {
        gaPairsArray = parsed.pairs;
      } else if (parsed.results && Array.isArray(parsed.results)) {
        gaPairsArray = parsed.results;
      } else {
        // Try to convert object format to array format
        const objectKeys = Object.keys(parsed);
        const audienceKeys = objectKeys.filter(key => key.startsWith('audience_'));
        const genreKeys = objectKeys.filter(key => key.startsWith('genre_'));

        if (audienceKeys.length > 0 && genreKeys.length > 0) {
          gaPairsArray = [];
          for (let i = 1; i <= Math.min(audienceKeys.length, genreKeys.length); i++) {
            const audience = parsed[`audience_${i}`];
            const genre = parsed[`genre_${i}`];
            if (audience && genre) {
              gaPairsArray.push({ audience, genre });
            }
          }
        } else {
          throw new Error('Response is not an array and no recognized array property found');
        }
      }
    }

    // Validate the structure
    const validatedPairs = gaPairsArray.map((pair, index) => {
      if (!pair.genre || !pair.audience) {
        throw new Error(`GA pair ${index + 1} missing genre or audience`);
      }

      if (!pair.genre.title || !pair.genre.description || !pair.audience.title || !pair.audience.description) {
        throw new Error(`GA pair ${index + 1} missing required fields`);
      }

      return {
        genre: {
          title: String(pair.genre.title).trim(),
          description: String(pair.genre.description).trim()
        },
        audience: {
          title: String(pair.audience.title).trim(),
          description: String(pair.audience.description).trim()
        }
      };
    });

    // Ensure we have exactly 5 pairs
    if (validatedPairs.length !== 5) {
      logger.warn(`Expected 5 GA pairs, got ${validatedPairs.length}. Using first 5 or padding with fallbacks.`);

      // If we have more than 5, take the first 5
      if (validatedPairs.length > 5) {
        return validatedPairs.slice(0, 5);
      }

      // If we have fewer than 5, pad with fallbacks
      const fallbacks = getFallbackGaPairs();
      while (validatedPairs.length < 5) {
        validatedPairs.push(fallbacks[validatedPairs.length]);
      }
    }

    logger.info(`Successfully parsed ${validatedPairs.length} GA pairs`);
    return validatedPairs;
  } catch (error) {
    logger.error('Failed to parse GA response:', error);
    logger.error('Raw response:', response);

    // Return fallback GA pairs if parsing fails
    logger.info('Using fallback GA pairs due to parsing failure');
    return getFallbackGaPairs();
  }
}

/**
 * Get fallback GA pairs when generation fails
 * @returns {Array} - Default GA pairs
 */
function getFallbackGaPairs() {
  return [
    {
      genre: {
        title: '学术研究',
        description: '学术性、研究导向的内容，具有正式的语调和详细的分析'
      },
      audience: {
        title: '研究人员',
        description: '寻求深入知识的学术研究人员和研究生'
      }
    },
    {
      genre: {
        title: '教育指南',
        description: '结构化的学习材料，具有清晰的解释和示例'
      },
      audience: {
        title: '学生',
        description: '本科生和该主题的新学习者'
      }
    },
    {
      genre: {
        title: '专业手册',
        description: '实用、以实施为重点的内容，用于工作场所应用'
      },
      audience: {
        title: '从业者',
        description: '在实践中应用知识的行业专业人员'
      }
    },
    {
      genre: {
        title: '科普文章',
        description: '使复杂主题易于理解的可访问内容'
      },
      audience: {
        title: '普通公众',
        description: '没有专业背景的好奇读者'
      }
    },
    {
      genre: {
        title: '技术文档',
        description: '详细的规范和实施指南'
      },
      audience: {
        title: '开发人员',
        description: '技术专家和系统实施人员'
      }
    }
  ];
}

'use client';

import { useState, useEffect, useCallback, useMemo, memo } from 'react';
import { useTranslation } from 'react-i18next';
import {
  Box,
  Typography,
  Paper,
  List,
  ListItem,
  ListItemText,
  Checkbox,
  IconButton,
  Collapse,
  Chip,
  Tooltip,
  Divider,
  CircularProgress
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import DeleteIcon from '@mui/icons-material/Delete';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import EditIcon from '@mui/icons-material/Edit';
import FolderIcon from '@mui/icons-material/Folder';
import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
import { useGenerateDataset } from '@/hooks/useGenerateDataset';
import axios from 'axios';

/**
 * 问题树视图组件
 * @param {Object} props
 * @param {Array} props.tags - 标签树
 * @param {Array} props.selectedQuestions - 已选择的问题ID列表
 * @param {Function} props.onSelectQuestion - 选择问题的回调函数
 * @param {Function} props.onDeleteQuestion - 删除问题的回调函数
 */
export default function QuestionTreeView({
  tags = [],
  selectedQuestions = [],
  onSelectQuestion,
  onDeleteQuestion,
  onEditQuestion,
  projectId,
  searchTerm
}) {
  const { t } = useTranslation();
  const [expandedTags, setExpandedTags] = useState({});
  const [questionsByTag, setQuestionsByTag] = useState({});
  const [processingQuestions, setProcessingQuestions] = useState({});
  const { generateSingleDataset } = useGenerateDataset();
  const [questions, setQuestions] = useState([]);
  const [loadedTags, setLoadedTags] = useState({});
  // 初始化时，将所有标签设置为收起状态（而不是展开状态）
  useEffect(() => {
    async function fetchTagsInfo() {
      try {
        // 获取标签信息，仅用于标签统计
        const response = await axios.get(`/api/projects/${projectId}/questions/tree?tagsOnly=true&input=${searchTerm}`);
        setQuestions(response.data); // 设置数据仅用于标签统计

        // 当搜索条件变化时，重新加载已展开标签的问题数据
        const expandedTagLabels = Object.entries(expandedTags)
          .filter(([_, isExpanded]) => isExpanded)
          .map(([label]) => label);

        // 重新加载已展开标签的数据
        for (const label of expandedTagLabels) {
          fetchTagQuestions(label);
        }
      } catch (error) {
        console.error('获取标签信息失败:', error);
      }
    }

    if (projectId) {
      fetchTagsInfo();
    }

    const initialExpandedState = {};
    const processTag = tag => {
      // 将默认状态改为 false（收起）而不是 true（展开）
      initialExpandedState[tag.label] = false;
      if (tag.child && tag.child.length > 0) {
        tag.child.forEach(processTag);
      }
    };

    tags.forEach(processTag);
    // 未分类问题也默认收起
    initialExpandedState['uncategorized'] = false;
    setExpandedTags(initialExpandedState);
  }, [tags]);

  // 根据标签对问题进行分类
  useEffect(() => {
    const taggedQuestions = {};

    // 初始化标签映射
    const initTagMap = tag => {
      taggedQuestions[tag.label] = [];
      if (tag.child && tag.child.length > 0) {
        tag.child.forEach(initTagMap);
      }
    };

    tags.forEach(initTagMap);

    // 将问题分配到对应的标签下
    questions.forEach(question => {
      // 如果问题没有标签，添加到"未分类"
      if (!question.label) {
        if (!taggedQuestions['uncategorized']) {
          taggedQuestions['uncategorized'] = [];
        }
        taggedQuestions['uncategorized'].push(question);
        return;
      }

      // 将问题添加到匹配的标签下
      const questionLabel = question.label;

      // 查找最精确匹配的标签
      // 使用一个数组来存储所有匹配的标签路径，以便找到最精确的匹配
      const findAllMatchingTags = (tag, path = []) => {
        const currentPath = [...path, tag.label];

        // 存储所有匹配结果
        const matches = [];

        // 精确匹配当前标签
        if (tag.label === questionLabel) {
          matches.push({ label: tag.label, depth: currentPath.length });
        }

        // 检查子标签
        if (tag.child && tag.child.length > 0) {
          for (const childTag of tag.child) {
            const childMatches = findAllMatchingTags(childTag, currentPath);
            matches.push(...childMatches);
          }
        }

        return matches;
      };

      // 在所有根标签中查找所有匹配
      let allMatches = [];
      for (const rootTag of tags) {
        const matches = findAllMatchingTags(rootTag);
        allMatches.push(...matches);
      }

      // 找到深度最大的匹配（最精确的匹配）
      let matchedTagLabel = null;
      if (allMatches.length > 0) {
        // 按深度排序，深度最大的是最精确的匹配
        allMatches.sort((a, b) => b.depth - a.depth);
        matchedTagLabel = allMatches[0].label;
      }

      if (matchedTagLabel) {
        // 如果找到匹配的标签，将问题添加到该标签下
        if (!taggedQuestions[matchedTagLabel]) {
          taggedQuestions[matchedTagLabel] = [];
        }
        taggedQuestions[matchedTagLabel].push(question);
      } else {
        // 如果找不到匹配的标签，添加到"未分类"
        if (!taggedQuestions['uncategorized']) {
          taggedQuestions['uncategorized'] = [];
        }
        taggedQuestions['uncategorized'].push(question);
      }
    });

    setQuestionsByTag(taggedQuestions);
  }, [questions, tags]);

  // 处理展开/折叠标签 - 使用 useCallback 优化
  const handleToggleExpand = useCallback(
    tagLabel => {
      // 检查是否需要加载此标签的问题数据
      const shouldExpand = !expandedTags[tagLabel];

      if (shouldExpand && !loadedTags[tagLabel]) {
        // 如果要展开且尚未加载数据，则加载数据
        fetchTagQuestions(tagLabel);
      }

      setExpandedTags(prev => ({
        ...prev,
        [tagLabel]: shouldExpand
      }));
    },
    [expandedTags, loadedTags, projectId]
  );

  // 获取特定标签的问题数据
  const fetchTagQuestions = useCallback(
    async tagLabel => {
      try {
        const response = await axios.get(
          `/api/projects/${projectId}/questions/tree?tag=${encodeURIComponent(tagLabel)}${searchTerm ? `&input=${searchTerm}` : ''}`
        );

        // 更新问题数据，合并新获取的数据
        setQuestions(prev => {
          // 创建一个新数组，包含现有数据
          const updatedQuestions = [...prev];

          // 添加新获取的问题数据
          response.data.forEach(newQuestion => {
            // 检查是否已存在相同 ID 的问题
            const existingIndex = updatedQuestions.findIndex(q => q.id === newQuestion.id);
            if (existingIndex === -1) {
              // 如果不存在，添加到数组
              updatedQuestions.push(newQuestion);
            } else {
              // 如果已存在，更新数据
              updatedQuestions[existingIndex] = newQuestion;
            }
          });

          return updatedQuestions;
        });

        // 标记该标签已加载数据
        setLoadedTags(prev => ({
          ...prev,
          [tagLabel]: true
        }));
      } catch (error) {
        console.error(`获取标签 "${tagLabel}" 的问题失败:`, error);
      }
    },
    [projectId, searchTerm, expandedTags]
  );

  // 检查问题是否被选中 - 使用 useCallback 优化
  const isQuestionSelected = useCallback(
    questionKey => {
      return selectedQuestions.includes(questionKey);
    },
    [selectedQuestions]
  );

  // 处理生成数据集 - 使用 useCallback 优化
  const handleGenerateDataset = async (questionId, questionInfo) => {
    // 设置处理状态
    setProcessingQuestions(prev => ({
      ...prev,
      [questionId]: true
    }));
    await generateSingleDataset({ projectId, questionId, questionInfo });
    // 重置处理状态
    setProcessingQuestions(prev => ({
      ...prev,
      [questionId]: false
    }));
  };

  // 渲染单个问题项 - 使用 useCallback 优化
  const renderQuestionItem = useCallback(
    (question, index, total) => {
      const questionKey = question.id;
      return (
        <QuestionItem
          key={questionKey}
          question={question}
          index={index}
          total={total}
          isSelected={isQuestionSelected(questionKey)}
          onSelect={onSelectQuestion}
          onDelete={onDeleteQuestion}
          onGenerate={handleGenerateDataset}
          onEdit={onEditQuestion}
          isProcessing={processingQuestions[questionKey]}
          t={t}
        />
      );
    },
    [isQuestionSelected, onSelectQuestion, onDeleteQuestion, handleGenerateDataset, processingQuestions, t]
  );

  // 计算标签及其子标签下的所有问题数量 - 使用 useMemo 缓存计算结果
  const tagQuestionCounts = useMemo(() => {
    const counts = {};

    const countQuestions = tag => {
      const directQuestions = questionsByTag[tag.label] || [];
      let total = directQuestions.length;

      if (tag.child && tag.child.length > 0) {
        for (const childTag of tag.child) {
          total += countQuestions(childTag);
        }
      }

      counts[tag.label] = total;
      return total;
    };

    tags.forEach(countQuestions);
    return counts;
  }, [questionsByTag, tags]);

  // 递归渲染标签树 - 使用 useCallback 优化
  const renderTagTree = useCallback(
    (tag, level = 0) => {
      const questions = questionsByTag[tag.label] || [];
      const hasQuestions = questions.length > 0;
      const hasChildren = tag.child && tag.child.length > 0;
      const isExpanded = expandedTags[tag.label];
      const totalQuestions = tagQuestionCounts[tag.label] || 0;

      return (
        <Box key={tag.label}>
          <TagItem
            tag={tag}
            level={level}
            isExpanded={isExpanded}
            totalQuestions={totalQuestions}
            onToggle={handleToggleExpand}
            t={t}
          />

          {/* 只有当标签展开时才渲染子内容，减少不必要的渲染 */}
          {isExpanded && (
            <Collapse in={true}>
              {hasChildren && (
                <List disablePadding>{tag.child.map(childTag => renderTagTree(childTag, level + 1))}</List>
              )}

              {hasQuestions && (
                <List disablePadding sx={{ mt: hasChildren ? 1 : 0 }}>
                  {questions.map((question, index) => renderQuestionItem(question, index, questions.length))}
                </List>
              )}
            </Collapse>
          )}
        </Box>
      );
    },
    [questionsByTag, expandedTags, tagQuestionCounts, handleToggleExpand, renderQuestionItem, t]
  );

  // 渲染未分类问题
  const renderUncategorizedQuestions = () => {
    const uncategorizedQuestions = questionsByTag['uncategorized'] || [];
    if (uncategorizedQuestions.length === 0) return null;

    return (
      <Box>
        <ListItem
          button
          onClick={() => handleToggleExpand('uncategorized')}
          sx={{
            py: 1,
            bgcolor: 'primary.light',
            color: 'primary.contrastText',
            '&:hover': {
              bgcolor: 'primary.main'
            },
            borderRadius: '4px',
            mb: 0.5,
            pr: 1
          }}
        >
          <FolderIcon fontSize="small" sx={{ mr: 1, color: 'inherit' }} />
          <ListItemText
            primary={
              <Box sx={{ display: 'flex', alignItems: 'center' }}>
                <Typography variant="body1" sx={{ fontWeight: 600, fontSize: '1rem' }}>
                  {t('datasets.uncategorized')}
                </Typography>
                <Chip
                  label={t('datasets.questionCount', { count: uncategorizedQuestions.length })}
                  size="small"
                  sx={{ ml: 1, height: 20, fontSize: '0.7rem', color: '#fff', backgroundColor: '#333' }}
                />
              </Box>
            }
          />
          <IconButton size="small" edge="end" sx={{ color: 'inherit' }}>
            {expandedTags['uncategorized'] ? <ExpandLessIcon /> : <ExpandMoreIcon />}
          </IconButton>
        </ListItem>

        <Collapse in={expandedTags['uncategorized']}>
          <List disablePadding>
            {uncategorizedQuestions.map((question, index) =>
              renderQuestionItem(question, index, uncategorizedQuestions.length)
            )}
          </List>
        </Collapse>
      </Box>
    );
  };

  // 如果没有标签和问题，显示空状态
  if (tags.length === 0 && Object.keys(questionsByTag).length === 0) {
    return (
      <Box sx={{ p: 4, textAlign: 'center' }}>
        <Typography variant="body1" color="text.secondary">
          {t('datasets.noTagsAndQuestions')}
        </Typography>
      </Box>
    );
  }

  return (
    <Paper
      elevation={0}
      sx={{
        border: '1px solid',
        borderColor: 'divider',
        borderRadius: 1,
        overflow: 'auto',
        p: 2,
        maxHeight: '75vh'
      }}
    >
      <List disablePadding>
        {renderUncategorizedQuestions()}
        {tags.map(tag => renderTagTree(tag))}
      </List>
    </Paper>
  );
}

// 使用 memo 优化问题项渲染
const QuestionItem = memo(
  ({ question, index, total, isSelected, onSelect, onDelete, onGenerate, onEdit, isProcessing, t }) => {
    const questionKey = question.id;
    return (
      <Box key={question.id}>
        <ListItem
          sx={{
            pl: 4,
            py: 1,
            borderRadius: '4px',
            ml: 2,
            mr: 1,
            mb: 0.5,
            bgcolor: isSelected ? 'action.selected' : 'transparent',
            '&:hover': {
              bgcolor: 'action.hover'
            }
          }}
        >
          <Checkbox checked={isSelected} onChange={() => onSelect(questionKey)} size="small" />
          <QuestionMarkIcon fontSize="small" sx={{ mr: 1, color: 'primary.main' }} />
          <ListItemText
            primary={
              <Typography variant="body2" sx={{ fontWeight: 400 }}>
                {question.question}
                {question.dataSites && question.dataSites.length > 0 && (
                  <Chip
                    label={t('datasets.answerCount', { count: question.dataSites.length })}
                    size="small"
                    color="primary"
                    variant="outlined"
                    sx={{ ml: 1, fontSize: '0.75rem', maxWidth: 150 }}
                  />
                )}
              </Typography>
            }
            secondary={
              <Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
                {t('datasets.source')}: {question.chunk?.name || question.chunkId || t('common.unknown')}
              </Typography>
            }
          />
          <Box>
            <Tooltip title={t('common.edit')}>
              <IconButton
                size="small"
                sx={{ mr: 1 }}
                onClick={() =>
                  onEdit({
                    question: question.question,
                    chunkId: question.chunkId,
                    label: question.label || 'other'
                  })
                }
                disabled={isProcessing}
              >
                <EditIcon fontSize="small" />
              </IconButton>
            </Tooltip>
            <Tooltip title={t('datasets.generateDataset')}>
              <IconButton
                size="small"
                sx={{ mr: 1 }}
                onClick={() => onGenerate(question.id, question.question)}
                disabled={isProcessing}
              >
                {isProcessing ? <CircularProgress size={16} /> : <AutoFixHighIcon fontSize="small" />}
              </IconButton>
            </Tooltip>
            <Tooltip title={t('common.delete')}>
              <IconButton size="small" onClick={() => onDelete(question.question, question.chunkId)}>
                <DeleteIcon fontSize="small" />
              </IconButton>
            </Tooltip>
          </Box>
        </ListItem>
        {index < total - 1 && <Divider component="li" variant="inset" sx={{ ml: 6 }} />}
      </Box>
    );
  }
);

// 使用 memo 优化标签项渲染
const TagItem = memo(({ tag, level, isExpanded, totalQuestions, onToggle, t }) => {
  return (
    <ListItem
      button
      onClick={() => onToggle(tag.label)}
      sx={{
        pl: level * 2 + 1,
        py: 1,
        bgcolor: level === 0 ? 'primary.light' : 'background.paper',
        color: level === 0 ? 'primary.contrastText' : 'inherit',
        '&:hover': {
          bgcolor: level === 0 ? 'primary.main' : 'action.hover'
        },
        borderRadius: '4px',
        mb: 0.5,
        pr: 1
      }}
    >
      {/* 内部内容保持不变 */}
      <FolderIcon fontSize="small" sx={{ mr: 1, color: level === 0 ? 'inherit' : 'primary.main' }} />
      <ListItemText
        primary={
          <Box sx={{ display: 'flex', alignItems: 'center' }}>
            <Typography
              variant="body1"
              sx={{
                fontWeight: level === 0 ? 600 : 400,
                fontSize: level === 0 ? '1rem' : '0.9rem'
              }}
            >
              {tag.label}
            </Typography>
            {totalQuestions > 0 && (
              <Chip
                label={t('datasets.questionCount', { count: totalQuestions })}
                size="small"
                color={level === 0 ? 'default' : 'primary'}
                variant={level === 0 ? 'default' : 'outlined'}
                sx={{ ml: 1, height: 20, fontSize: '0.7rem', color: '#fff', backgroundColor: '#333' }}
              />
            )}
          </Box>
        }
      />
      <IconButton size="small" edge="end" sx={{ color: level === 0 ? 'inherit' : 'action.active' }}>
        {isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
      </IconButton>
    </ListItem>
  );
});

import { NextResponse } from 'next/server';
import { getTags, createTag, updateTag, deleteTag } from '@/lib/db/tags';
import { getQuestionsByTagName } from '@/lib/db/questions';

// 获取项目的标签树
export async function GET(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: 'Project ID is required' }, { status: 400 });
    }

    // 获取标签树
    const tags = await getTags(projectId);

    return NextResponse.json({ tags });
  } catch (error) {
    console.error('Failed to obtain the label tree:', String(error));
    return NextResponse.json({ error: error.message || 'Failed to obtain the label tree' }, { status: 500 });
  }
}

// 更新项目的标签树
export async function PUT(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: 'Project ID is required' }, { status: 400 });
    }

    // 获取请求体
    const { tags } = await request.json();
    if (tags.id === undefined || tags.id === null || tags.id === '') {
      console.log('createTag', tags);
      let res = await createTag(projectId, tags.label, tags.parentId);
      return NextResponse.json({ tags: res });
    } else {
      let res = await updateTag(tags.label, tags.id);
      return NextResponse.json({ tags: res });
    }
  } catch (error) {
    console.error('Failed to update tags:', String(error));
    return NextResponse.json({ error: error.message || 'Failed to update tags' }, { status: 500 });
  }
}

export async function POST(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: 'Project ID is required' }, { status: 400 });
    }
    const { tagName } = await request.json();
    console.log('tagName', tagName);
    let data = await getQuestionsByTagName(projectId, tagName);

    return NextResponse.json(data);
  } catch (error) {
    console.error('Failed to obtain the label tree:', String(error));
    return NextResponse.json({ error: error.message || 'Failed to obtain the label tree' }, { status: 500 });
  }
}

export async function DELETE(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: 'Project ID is required' }, { status: 400 });
    }

    // 获取要删除的标签ID
    const { searchParams } = new URL(request.url);
    const id = searchParams.get('id');

    if (!id) {
      return NextResponse.json({ error: '标签 ID 是必需的' }, { status: 400 });
    }

    console.log(`正在删除标签: ${id}`);
    const result = await deleteTag(id);
    console.log(`删除标签成功: ${id}`);

    return NextResponse.json({ success: true, message: '删除标签成功', data: result });
  } catch (error) {
    console.error('删除标签失败:', String(error));
    return NextResponse.json(
      {
        error: error.message || '删除标签失败',
        success: false
      },
      { status: 500 }
    );
  }
}

'use client';

import {
  Box,
  Button,
  Typography,
  List,
  ListItem,
  ListItemText,
  Divider,
  CircularProgress,
  Tooltip
} from '@mui/material';
import UploadFileIcon from '@mui/icons-material/UploadFile';
import DeleteIcon from '@mui/icons-material/Delete';
import { alpha } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';
import React, { useRef, useState } from 'react';

export default function UploadArea({
  theme,
  files,
  uploading,
  uploadedFiles,
  onFileSelect,
  onRemoveFile,
  onUpload,
  selectedModel
}) {
  const { t } = useTranslation();
  const [dragActive, setDragActive] = useState(false);
  const inputRef = useRef(null);

  // 拖拽进入
  const handleDragOver = e => {
    e.preventDefault();
    e.stopPropagation();
    if (!dragActive) setDragActive(true);
  };
  // 拖拽离开
  const handleDragLeave = e => {
    e.preventDefault();
    e.stopPropagation();
    setDragActive(false);
  };
  // 拖拽释放
  const handleDrop = e => {
    e.preventDefault();
    e.stopPropagation();
    setDragActive(false);
    if (!selectedModel?.id || uploading) return;
    const files = e.dataTransfer.files;
    if (files && files.length > 0) {
      // 构造一个模拟的 event 以复用 onFileSelect
      const event = { target: { files } };
      onFileSelect(event);
    }
  };

  return (
    <Box
      sx={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        p: 3,
        height: '100%',
        border: `2px dashed ${dragActive ? theme.palette.primary.main : alpha(theme.palette.primary.main, 0.2)}`,
        borderRadius: 2,
        bgcolor: dragActive ? alpha(theme.palette.primary.main, 0.12) : alpha(theme.palette.primary.main, 0.05),
        transition: 'all 0.3s ease',
        '&:hover': {
          bgcolor: alpha(theme.palette.primary.main, 0.08),
          borderColor: alpha(theme.palette.primary.main, 0.3)
        },
        cursor: uploading || !selectedModel?.id ? 'not-allowed' : 'pointer',
        position: 'relative'
      }}
      onDragOver={handleDragOver}
      onDragLeave={handleDragLeave}
      onDrop={handleDrop}
    >
      {dragActive && (
        <Box
          sx={{
            position: 'absolute',
            top: 0,
            left: 0,
            width: '100%',
            height: '100%',
            bgcolor: alpha(theme.palette.primary.main, 0.3),
            zIndex: 10,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            pointerEvents: 'none',
            borderRadius: 2,
            border: `3px solid ${theme.palette.primary.main}`,
            backdropFilter: 'blur(2px)'
          }}
        >
          <Box
            sx={{
              bgcolor: 'rgba(255, 255, 255, 0.9)',
              p: 3,
              borderRadius: 1,
              boxShadow: '0 4px 8px rgba(0,0,0,0.1)',
              textAlign: 'center',
              display: 'flex',
              flexDirection: 'column',
              alignItems: 'center',
              border: `1px solid ${theme.palette.primary.main}`
            }}
          >
            <UploadFileIcon color="primary" sx={{ fontSize: 40, mb: 1 }} />
            <Typography variant="h6" color="primary" sx={{ fontWeight: 'bold' }}>
              {t('textSplit.dragToUpload', { defaultValue: '拖拽文件到此处上传' })}
            </Typography>
          </Box>
        </Box>
      )}
      <Typography variant="subtitle1" gutterBottom>
        {t('textSplit.uploadNewDocument')}
      </Typography>

      <Tooltip
        title={!selectedModel?.id ? t('textSplit.selectModelFirst', { defaultValue: '请先在右上角选择模型' }) : ''}
      >

          <Button
            component="label"
            variant="contained"
            startIcon={<UploadFileIcon />}
            sx={{ mb: 2, mt: 2 }}
            disabled={!selectedModel?.id || uploading}
          >
            {t('textSplit.selectFile')}
            <input
              type="file"
              hidden
              accept=".md,.txt,.docx,.pdf"
              multiple
              onChange={onFileSelect}
              disabled={!selectedModel?.id || uploading}
            />
          </Button>

      </Tooltip>

      <Typography variant="body2" color="textSecondary">
        {uploadedFiles.total > 0 ? t('textSplit.mutilFileMessage') : t('textSplit.supportedFormats')}
      </Typography>

      {files.length > 0 && (
        <Box sx={{ mt: 3, width: '100%' }}>
          <Typography variant="subtitle2" gutterBottom>
            {t('textSplit.selectedFiles', { count: files.length })}
          </Typography>

          <List sx={{ bgcolor: theme.palette.background.paper, borderRadius: 1, maxHeight: '200px', overflow: 'auto' }}>
            {files.map((file, index) => (
              <Box key={index}>
                <ListItem
                  secondaryAction={
                    <Button
                      size="small"
                      color="error"
                      startIcon={<DeleteIcon />}
                      onClick={() => onRemoveFile(index)}
                      disabled={uploading}
                    >
                      {t('common.delete')}
                    </Button>
                  }
                >
                  <ListItemText primary={file.name} secondary={`${(file.size / 1024).toFixed(2)} KB`} />
                </ListItem>
                {index < files.length - 1 && <Divider />}
              </Box>
            ))}
          </List>

          <Box sx={{ mt: 3, display: 'flex', justifyContent: 'center' }}>
            <Tooltip
              title={
                !selectedModel?.id ? t('textSplit.selectModelFirst', { defaultValue: '请先在右上角选择模型' }) : ''
              }
            >

                <Button
                  variant="contained"
                  color="primary"
                  onClick={onUpload}
                  disabled={uploading || !selectedModel?.id}
                  sx={{ minWidth: 120 }}
                >
                  {uploading ? <CircularProgress size={24} /> : t('textSplit.uploadAndProcess')}
                </Button>

            </Tooltip>
          </Box>
        </Box>
      )}
    </Box>
  );
}

import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { DEFAULT_SETTINGS } from '@/constant/setting';

export default function useTaskSettings(projectId) {
  const { t } = useTranslation();
  const [taskSettings, setTaskSettings] = useState({
    ...DEFAULT_SETTINGS
  });
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);

  useEffect(() => {
    async function fetchTaskSettings() {
      try {
        setLoading(true);
        const response = await fetch(`/api/projects/${projectId}/tasks`);
        if (!response.ok) {
          throw new Error(t('settings.fetchTasksFailed'));
        }

        const data = await response.json();

        // 如果没有配置，使用默认值
        if (Object.keys(data).length === 0) {
          setTaskSettings({
            ...DEFAULT_SETTINGS
          });
        } else {
          setTaskSettings({
            ...DEFAULT_SETTINGS,
            ...data
          });
        }
      } catch (error) {
        console.error('获取任务配置出错:', error);
        setError(error.message);
      } finally {
        setLoading(false);
      }
    }

    fetchTaskSettings();
  }, [projectId, t]);

  return {
    taskSettings,
    setTaskSettings,
    loading,
    error,
    success,
    setSuccess
  };
}

import { NextResponse } from 'next/server';
import path from 'path';
import fs from 'fs';
import { getProjectRoot } from '@/lib/db/base';
import { getDatasets } from '@/lib/db/datasets';

export async function POST(request, { params }) {
  try {
    const { projectId } = params;
    const { formatType, systemPrompt, confirmedOnly, includeCOT } = await request.json();

    if (!projectId) {
      return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
    }

    // 获取项目根目录
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);
    const configPath = path.join(projectPath, 'dataset_info.json');
    const alpacaPath = path.join(projectPath, 'alpaca.json');
    const sharegptPath = path.join(projectPath, 'sharegpt.json');

    // 获取数据集
    let datasets = await getDatasets(projectId, !!confirmedOnly);

    // 创建 dataset_info.json 配置
    const config = {
      [`[Easy Dataset] [${projectId}] Alpaca`]: {
        file_name: 'alpaca.json',
        columns: {
          prompt: 'instruction',
          query: 'input',
          response: 'output',
          system: 'system'
        }
      },
      [`[Easy Dataset] [${projectId}] ShareGPT`]: {
        file_name: 'sharegpt.json',
        formatting: 'sharegpt',
        columns: {
          messages: 'messages'
        },
        tags: {
          role_tag: 'role',
          content_tag: 'content',
          user_tag: 'user',
          assistant_tag: 'assistant',
          system_tag: 'system'
        }
      }
    };

    // 生成数据文件
    const alpacaData = datasets.map(({ question, answer, cot }) => ({
      instruction: question,
      input: '',
      output: cot && includeCOT ? `<think>${cot}</think>\n${answer}` : answer,
      system: systemPrompt || ''
    }));

    const sharegptData = datasets.map(({ question, answer, cot }) => {
      const messages = [];
      if (systemPrompt) {
        messages.push({
          role: 'system',
          content: systemPrompt
        });
      }
      messages.push({
        role: 'user',
        content: question
      });
      messages.push({
        role: 'assistant',
        content: cot && includeCOT ? `<think>${cot}</think>\n${answer}` : answer
      });
      return { messages };
    });

    // 写入文件
    await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2));
    await fs.promises.writeFile(alpacaPath, JSON.stringify(alpacaData, null, 2));
    await fs.promises.writeFile(sharegptPath, JSON.stringify(sharegptData, null, 2));

    return NextResponse.json({
      success: true,
      configPath,
      files: [
        { path: alpacaPath, format: 'alpaca' },
        { path: sharegptPath, format: 'sharegpt' }
      ]
    });
  } catch (error) {
    console.error('Error generating Llama Factory config:', String(error));
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

import { NextResponse } from 'next/server';
import { exec } from 'child_process';
import path from 'path';
import fs from 'fs';

// 执行更新脚本
export async function POST() {
  try {
    // 检查是否在客户端环境中运行
    const desktopDir = path.join(process.cwd(), 'desktop');
    const updaterPath = path.join(desktopDir, 'scripts', 'updater.js');

    if (!fs.existsSync(updaterPath)) {
      return NextResponse.json(
        {
          success: false,
          message: '更新功能仅在客户端环境中可用'
        },
        { status: 400 }
      );
    }

    // 执行更新脚本
    return new Promise(resolve => {
      const updaterProcess = exec(`node "${updaterPath}"`, { cwd: process.cwd() });

      let output = '';

      updaterProcess.stdout.on('data', data => {
        output += data.toString();
        console.log(`Update output: ${data}`);
      });

      updaterProcess.stderr.on('data', data => {
        output += data.toString();
        console.error(`Update error: ${data}`);
      });

      updaterProcess.on('close', code => {
        console.log(`Update process exit, exit code: ${code}`);

        if (code === 0) {
          resolve(
            NextResponse.json({
              success: true,
              message: 'Update successful, application will restart'
            })
          );
        } else {
          resolve(
            NextResponse.json(
              {
                success: false,
                message: `Update failed, exit code: ${code}, output: ${output}`
              },
              { status: 500 }
            )
          );
        }
      });
    });
  } catch (error) {
    console.error('Failed to execute update:', String(error));
    return NextResponse.json(
      {
        success: false,
        message: `Failed to execute update: ${error.message}`
      },
      { status: 500 }
    );
  }
}

import { NextResponse } from 'next/server';
import {
  deleteDataset,
  getDatasetsByPagination,
  getDatasetsIds,
  getDatasetsById,
  updateDataset
} from '@/lib/db/datasets';
import datasetService from '@/lib/services/datasets';

// 优化思维链函数已移至服务层

/**
 * 生成数据集（为单个问题生成答案）
 */
export async function POST(request, { params }) {
  try {
    const { projectId } = params;
    const { questionId, model, language } = await request.json();

    // 使用数据集生成服务
    const result = await datasetService.generateDatasetForQuestion(projectId, questionId, {
      model,
      language
    });

    return NextResponse.json(result);
  } catch (error) {
    console.error('Failed to generate dataset:', String(error));
    return NextResponse.json(
      {
        error: error.message || 'Failed to generate dataset'
      },
      { status: 500 }
    );
  }
}

/**
 * 获取项目的所有数据集
 */
export async function GET(request, { params }) {
  try {
    const { projectId } = params;
    const { searchParams } = new URL(request.url);
    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: '项目ID不能为空' }, { status: 400 });
    }
    let status = searchParams.get('status');
    let confirmed = undefined;
    if (status === 'confirmed') confirmed = true;
    if (status === 'unconfirmed') confirmed = false;

    let selectedAll = searchParams.get('selectedAll');
    // 获取搜索字段参数
    const field = searchParams.get('field') || 'question';
    // 获取思维链筛选参数
    const hasCot = searchParams.get('hasCot') || 'all';
    if (selectedAll) {
      let data = await getDatasetsIds(projectId, confirmed, searchParams.get('input'), field, hasCot);
      return NextResponse.json(data);
    }

    // 获取数据集
    const datasets = await getDatasetsByPagination(
      projectId,
      parseInt(searchParams.get('page')),
      parseInt(searchParams.get('size')),
      confirmed,
      searchParams.get('input'),
      field, // 传递搜索字段参数
      hasCot // 传递思维链筛选参数
    );

    return NextResponse.json(datasets);
  } catch (error) {
    console.error('获取数据集失败:', String(error));
    return NextResponse.json(
      {
        error: error.message || '获取数据集失败'
      },
      { status: 500 }
    );
  }
}

/**
 * 删除数据集
 */
export async function DELETE(request) {
  try {
    const { searchParams } = new URL(request.url);
    const datasetId = searchParams.get('id');
    if (!datasetId) {
      return NextResponse.json(
        {
          error: 'Dataset ID cannot be empty'
        },
        { status: 400 }
      );
    }

    await deleteDataset(datasetId);

    return NextResponse.json({
      success: true,
      message: 'Dataset deleted successfully'
    });
  } catch (error) {
    console.error('Failed to delete dataset:', error);
    return NextResponse.json(
      {
        error: error.message || 'Failed to delete dataset'
      },
      { status: 500 }
    );
  }
}

/**
 * 编辑数据集
 */
export async function PATCH(request) {
  try {
    const { searchParams } = new URL(request.url);
    const datasetId = searchParams.get('id');
    const { answer, cot, confirmed } = await request.json();
    if (!datasetId) {
      return NextResponse.json(
        {
          error: 'Dataset ID cannot be empty'
        },
        { status: 400 }
      );
    }
    // 获取所有数据集
    let dataset = await getDatasetsById(datasetId);
    if (!dataset) {
      return NextResponse.json(
        {
          error: 'Dataset does not exist'
        },
        { status: 404 }
      );
    }
    let data = { id: datasetId };
    if (confirmed) data.confirmed = confirmed;
    if (answer) data.answer = answer;
    if (cot) data.cot = cot;

    // 保存更新后的数据集列表
    await updateDataset(data);

    return NextResponse.json({
      success: true,
      message: 'Dataset updated successfully',
      dataset: dataset
    });
  } catch (error) {
    console.error('Failed to update dataset:', String(error));
    return NextResponse.json(
      {
        error: error.message || 'Failed to update dataset'
      },
      { status: 500 }
    );
  }
}

import { NextResponse } from 'next/server';
import { distillTagsPrompt } from '@/lib/llm/prompts/distillTags';
import { distillTagsEnPrompt } from '@/lib/llm/prompts/distillTagsEn';
import { db } from '@/lib/db';
import { getProject } from '@/lib/db/projects';

const LLMClient = require('@/lib/llm/core');

/**
 * 生成标签接口：根据顶级主题、某级标签构造指定数量的子标签
 */
export async function POST(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: '项目ID不能为空' }, { status: 400 });
    }

    const { parentTag, parentTagId, tagPath, count = 10, model, language = 'zh' } = await request.json();

    if (!parentTag) {
      const errorMsg = language === 'en' ? 'Topic tag name cannot be empty' : '主题标签名称不能为空';
      return NextResponse.json({ error: errorMsg }, { status: 400 });
    }

    // 查询现有标签
    const existingTags = await db.tags.findMany({
      where: {
        projectId,
        parentId: parentTagId || null
      }
    });

    const existingTagNames = existingTags.map(tag => tag.label);

    // 创建LLM客户端
    const llmClient = new LLMClient(model);

    // 获取项目配置
    const project = await getProject(projectId);
    const { globalPrompt } = project || {};

    // 生成提示词
    const promptFunc = language === 'en' ? distillTagsEnPrompt : distillTagsPrompt;
    const prompt = promptFunc(tagPath, parentTag, existingTagNames, count, globalPrompt);

    // 调用大模型生成标签
    const { answer } = await llmClient.getResponseWithCOT(prompt);

    // 解析返回的标签
    let tags = [];

    try {
      tags = JSON.parse(answer);
    } catch (error) {
      console.error('解析标签JSON失败:', String(error));
      // 尝试使用正则表达式提取标签
      const matches = answer.match(/"([^"]+)"/g);
      if (matches) {
        tags = matches.map(match => match.replace(/"/g, ''));
      }
    }

    // 保存标签到数据库
    const savedTags = [];
    for (let i = 0; i < tags.length; i++) {
      const tagName = tags[i];
      try {
        const tag = await db.tags.create({
          data: {
            label: tagName,
            projectId,
            parentId: parentTagId || null
          }
        });
        savedTags.push(tag);
      } catch (error) {
        console.error(`[标签生成] 保存标签 ${tagName} 失败:`, String(error));
        throw error;
      }
    }
    return NextResponse.json(savedTags);
  } catch (error) {
    console.error('[标签生成] 生成标签失败:', String(error));
    console.error('[标签生成] 错误堆栈:', error.stack);
    return NextResponse.json({ error: error.message || '生成标签失败' }, { status: 500 });
  }
}

import { getProjectRoot } from '@/lib/db/base';
import { db } from '@/lib/db/index';
import fs from 'fs';
import path from 'path';
import { NextResponse } from 'next/server';

/**
 * 获取未迁移的项目列表
 * @returns {Promise<Response>} 包含未迁移项目列表的响应
 */
export async function GET(request) {
  // 获取当前请求的 URL，从中提取查询参数
  const { searchParams } = new URL(request.url);
  // 这行代码是关键，强制每次请求都是不同的
  const timestamp = searchParams.get('_t') || Date.now();
  try {
    // 获取项目根目录
    const projectRoot = await getProjectRoot();

    // 读取根目录下的所有文件夹（每个文件夹代表一个项目）
    const files = await fs.promises.readdir(projectRoot, { withFileTypes: true });

    // 过滤出目录类型的条目
    const projectDirs = files.filter(file => file.isDirectory());

    // 如果没有项目目录，则直接返回空列表
    if (projectDirs.length === 0) {
      return NextResponse.json({
        success: true,
        data: []
      });
    }

    // 获取所有项目ID
    const projectIds = projectDirs.map(dir => dir.name);

    // 批量查询已迁移的项目
    const existingProjects = await db.projects.findMany({
      where: {
        id: {
          in: projectIds
        }
      },
      select: {
        id: true
      }
    });

    // 转换为集合以便快速查找
    const existingProjectIds = new Set(existingProjects.map(p => p.id));

    // 筛选出未迁移的项目
    const unmigratedProjectDirs = projectDirs.filter(dir => !existingProjectIds.has(dir.name));

    // 获取未迁移项目的ID列表
    const unmigratedProjects = unmigratedProjectDirs.map(dir => dir.name);

    return NextResponse.json({
      success: true,
      data: unmigratedProjects,
      projectRoot,
      number: Date.now(),
      timestamp
    });
  } catch (error) {
    console.error('获取未迁移项目列表出错:', String(error));
    return NextResponse.json(
      {
        success: false,
        error: error.message
      },
      { status: 500 }
    );
  }
}

'use client';

import { useState, useEffect, useCallback, useRef } from 'react';
import {
  Box,
  Chip,
  Typography,
  IconButton,
  Tooltip,
  CircularProgress,
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  Button
} from '@mui/material';
import { Psychology as PsychologyIcon, AutoAwesome as AutoFixIcon } from '@mui/icons-material';
import { useTranslation } from 'react-i18next';
import GaPairsManager from './GaPairsManager';

/**
 * GA Pairs Indicator Component - Shows GA pairs status for a file
 * @param {Object} props
 * @param {string} props.projectId - Project ID
 * @param {string} props.fileId - File ID
 * @param {string} props.fileName - File name for display
 */
export default function GaPairsIndicator({ projectId, fileId, fileName = '未命名文件' }) {
  const { t } = useTranslation();
  const [gaPairs, setGaPairs] = useState([]);
  const [loading, setLoading] = useState(false);
  const [detailsOpen, setDetailsOpen] = useState(false);

  // 获取GA对状态的函数
  const fetchGaPairsStatus = useCallback(async () => {
    try {
      setLoading(true);

      const response = await fetch(`/api/projects/${projectId}/files/${fileId}/ga-pairs`);

      if (!response.ok) {
        if (response.status === 404) {
          setGaPairs([]);
          return;
        }
        throw new Error(`HTTP ${response.status}: Failed to load GA pairs`);
      }

      const result = await response.json();

      // 处理响应格式
      let newGaPairs = [];
      if (Array.isArray(result)) {
        newGaPairs = result;
      } else if (result?.data) {
        newGaPairs = result.data;
      }

      setGaPairs(newGaPairs);
    } catch (error) {
      console.error('获取GA对状态失败:', error);
      setGaPairs([]);
    } finally {
      setLoading(false);
    }
  }, [projectId, fileId]);

  // 初始加载
  useEffect(() => {
    if (projectId && fileId) {
      fetchGaPairsStatus();
    }
  }, [projectId, fileId, fetchGaPairsStatus]);

  //监听外部事件
  useEffect(() => {
    const handleRefresh = event => {
      const { projectId: eventProjectId, fileIds } = event.detail || {};

      if (eventProjectId === projectId && fileIds?.includes(String(fileId))) {
        fetchGaPairsStatus();
      }
    };

    window.addEventListener('refreshGaPairsIndicators', handleRefresh);
    return () => window.removeEventListener('refreshGaPairsIndicators', handleRefresh);
  }, [projectId, fileId, fetchGaPairsStatus]);

  // 计算激活的GA对数量
  const activePairs = gaPairs.filter(pair => pair.isActive);
  const hasGaPairs = gaPairs.length > 0;

  //GA对变化回调处理
  const handleGaPairsChange = useCallback(newGaPairs => {
    setGaPairs(newGaPairs || []);
  }, []);

  const handleOpenDialog = useCallback(() => {
    setDetailsOpen(true);
  }, []);

  const handleCloseDialog = useCallback(() => {
    setDetailsOpen(false);
  }, []);

  //加载状态显示
  if (loading) {
    return (
      <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
        <CircularProgress size={16} />
        <Typography variant="caption" color="textSecondary">
          Loading...
        </Typography>
      </Box>
    );
  }

  return (
    <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
      {hasGaPairs ? (
        <Chip
          icon={<PsychologyIcon />}
          label={`${activePairs.length}/${gaPairs.length} GA Pairs`}
          size="small"
          color={activePairs.length > 0 ? 'primary' : 'default'}
          variant={activePairs.length > 0 ? 'filled' : 'outlined'}
          onClick={handleOpenDialog}
        />
      ) : (
        <Tooltip title="Generate GA Pairs">
          <IconButton size="small" onClick={handleOpenDialog} color="primary">
            <AutoFixIcon fontSize="small" />
          </IconButton>
        </Tooltip>
      )}

      {/* Details Dialog */}
      <Dialog open={detailsOpen} onClose={handleCloseDialog} maxWidth="lg" fullWidth>
        <DialogTitle>GA Pairs for {fileName}</DialogTitle>
        <DialogContent>
          {detailsOpen && (
            <GaPairsManager projectId={projectId} fileId={fileId} onGaPairsChange={handleGaPairsChange} />
          )}
        </DialogContent>
        <DialogActions>
          <Button onClick={handleCloseDialog}>Close</Button>
        </DialogActions>
      </Dialog>
    </Box>
  );
}

'use server';
import { db } from '@/lib/db/index';

/**
 * 获取数据集列表(根据项目ID)
 * @param projectId 项目id
 * @param page
 * @param pageSize
 * @param confirmed
 * @param input
 * @param field 搜索字段，可选值：question, answer, cot, questionLabel
 * @param hasCot 思维链筛选，可选值：all, yes, no
 */
export async function getDatasetsByPagination(
  projectId,
  page = 1,
  pageSize = 10,
  confirmed,
  input,
  field = 'question',
  hasCot = 'all'
) {
  try {
    // 根据搜索字段构建查询条件
    const searchCondition = {};
    if (input) {
      if (field === 'question') {
        searchCondition.question = { contains: input };
      } else if (field === 'answer') {
        searchCondition.answer = { contains: input };
      } else if (field === 'cot') {
        searchCondition.cot = { contains: input };
      } else if (field === 'questionLabel') {
        searchCondition.questionLabel = { contains: input };
      }
    }

    // 思维链筛选条件
    const cotCondition = {};
    if (hasCot === 'yes') {
      cotCondition.cot = { not: '' };
    } else if (hasCot === 'no') {
      cotCondition.cot = '';
    }

    const whereClause = {
      projectId,
      ...(confirmed !== undefined && { confirmed: confirmed }),
      ...searchCondition,
      ...cotCondition
    };

    console.log(111, whereClause);

    const [data, total, confirmedCount] = await Promise.all([
      db.datasets.findMany({
        where: whereClause,
        orderBy: {
          createAt: 'desc'
        },
        skip: (page - 1) * pageSize,
        take: pageSize
      }),
      db.datasets.count({
        where: whereClause
      }),
      db.datasets.count({
        where: { ...whereClause, confirmed: true }
      })
    ]);

    return { data, total, confirmedCount };
  } catch (error) {
    console.error('Failed to get datasets by pagination in database');
    throw error;
  }
}

export async function getDatasets(projectId, confirmed) {
  try {
    const whereClause = {
      projectId,
      ...(confirmed !== undefined && { confirmed: confirmed })
    };
    return await db.datasets.findMany({
      where: whereClause,
      select: {
        question: true,
        answer: true,
        cot: true,
        questionLabel: true
      },
      orderBy: {
        createAt: 'desc'
      }
    });
  } catch (error) {
    console.error('Failed to get datasets in database');
    throw error;
  }
}

export async function getDatasetsIds(projectId, confirmed, input, field = 'question', hasCot = 'all') {
  try {
    // 根据搜索字段构建查询条件
    const searchCondition = {};
    if (input) {
      if (field === 'question') {
        searchCondition.question = { contains: input };
      } else if (field === 'answer') {
        searchCondition.answer = { contains: input };
      } else if (field === 'cot') {
        searchCondition.cot = { contains: input };
      } else if (field === 'questionLabel') {
        searchCondition.questionLabel = { contains: input };
      }
    }

    // 思维链筛选条件
    const cotCondition = {};
    if (hasCot === 'yes') {
      cotCondition.cot = { not: null };
      cotCondition.cot = { not: '' };
    } else if (hasCot === 'no') {
      cotCondition.OR = [{ cot: null }, { cot: '' }];
    }

    const whereClause = {
      projectId,
      ...(confirmed !== undefined && { confirmed: confirmed }),
      ...searchCondition,
      ...cotCondition
    };
    return await db.datasets.findMany({
      where: whereClause,
      select: {
        id: true
      },
      orderBy: {
        createAt: 'desc'
      }
    });
  } catch (error) {
    console.error('Failed to get datasets ids in database');
    throw error;
  }
}

/**
 * 获取数据集数量(根据项目ID)
 * @param projectId 项目id
 */
export async function getDatasetsCount(projectId) {
  try {
    return await db.datasets.count({
      where: {
        projectId
      }
    });
  } catch (error) {
    console.error('Failed to get datasets count by projectId in database');
    throw error;
  }
}

/**
 * 获取数据集数量(根据问题Id)
 * @param questionId 问题Id
 */
export async function getDatasetsCountByQuestionId(questionId) {
  try {
    return await db.datasets.count({
      where: {
        questionId
      }
    });
  } catch (error) {
    console.error('Failed to get datasets count by projectId in database');
    throw error;
  }
}

/**
 * 获取数据集详情
 * @param id 数据集id
 */
export async function getDatasetsById(id) {
  try {
    return await db.datasets.findUnique({
      where: { id }
    });
  } catch (error) {
    console.error('Failed to get datasets by id in database');
    throw error;
  }
}

/**
 * 保存数据集列表
 * @param dataset
 */
export async function createDataset(dataset) {
  try {
    return await db.datasets.create({
      data: dataset
    });
  } catch (error) {
    console.error('Failed to save datasets in database');
    throw error;
  }
}

export async function updateDataset(dataset) {
  try {
    return await db.datasets.update({
      data: dataset,
      where: {
        id: dataset.id
      }
    });
  } catch (error) {
    console.error('Failed to update datasets in database');
    throw error;
  }
}

export async function deleteDataset(datasetId) {
  try {
    return await db.datasets.delete({
      where: {
        id: datasetId
      }
    });
  } catch (error) {
    console.error('Failed to delete datasets in database');
    throw error;
  }
}

export async function getDatasetsCounts(projectId) {
  try {
    const [total, confirmedCount] = await Promise.all([
      db.datasets.count({
        where: { projectId }
      }),
      db.datasets.count({
        where: { projectId, confirmed: true }
      })
    ]);

    return { total, confirmedCount };
  } catch (error) {
    console.error('Failed to delete datasets in database');
    throw error;
  }
}

export async function getNavigationItems(projectId, datasetId, operateType) {
  const currentItem = await db.datasets.findUnique({
    where: { id: datasetId }
  });
  if (!currentItem) {
    throw new Error('当前记录不存在');
  }
  if (operateType === 'prev') {
    return await db.datasets.findFirst({
      where: { createAt: { gt: currentItem.createAt }, projectId },
      orderBy: { createAt: 'asc' }
    });
  } else {
    return await db.datasets.findFirst({
      where: { createAt: { lt: currentItem.createAt }, projectId },
      orderBy: { createAt: 'desc' }
    });
  }
}

'use client';

import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import axios from 'axios';

const useDatasetExport = projectId => {
  const { t } = useTranslation();

  // 导出数据集
  const exportDatasets = async exportOptions => {
    try {
      let apiUrl = `/api/projects/${projectId}/datasets/export`;
      if (exportOptions.confirmedOnly) {
        apiUrl += `?status=confirmed`;
      }
      const response = await axios.get(apiUrl);
      let dataToExport = response.data;

      // 根据选择的格式转换数据
      let formattedData;
      // 不同文件格式
      let mimeType = 'application/json';

      if (exportOptions.formatType === 'alpaca') {
        // 根据选择的字段类型生成不同的数据格式
        if (exportOptions.alpacaFieldType === 'instruction') {
          // 使用 instruction 字段
          formattedData = dataToExport.map(({ question, answer, cot }) => ({
            instruction: question,
            input: '',
            output: cot && exportOptions.includeCOT ? `<think>${cot}</think>\n${answer}` : answer,
            system: exportOptions.systemPrompt || ''
          }));
        } else {
          // 使用 input 字段
          formattedData = dataToExport.map(({ question, answer, cot }) => ({
            instruction: exportOptions.customInstruction || '',
            input: question,
            output: cot && exportOptions.includeCOT ? `<think>${cot}</think>\n${answer}` : answer,
            system: exportOptions.systemPrompt || ''
          }));
        }
      } else if (exportOptions.formatType === 'sharegpt') {
        formattedData = dataToExport.map(({ question, answer, cot }) => {
          const messages = [];

          // 添加系统提示词（如果有）
          if (exportOptions.systemPrompt) {
            messages.push({
              role: 'system',
              content: exportOptions.systemPrompt
            });
          }

          // 添加用户问题
          messages.push({
            role: 'user',
            content: question
          });

          // 添加助手回答
          messages.push({
            role: 'assistant',
            content: cot && exportOptions.includeCOT ? `<think>${cot}</think>\n${answer}` : answer
          });

          return { messages };
        });
      } else if (exportOptions.formatType === 'custom') {
        // 处理自定义格式
        const { questionField, answerField, cotField, includeLabels, includeChunk } = exportOptions.customFields;
        formattedData = dataToExport.map(({ question, answer, cot, questionLabel: labels, chunkId }) => {
          const item = {
            [questionField]: question,
            [answerField]: answer
          };

          // 如果有思维链且用户选择包含思维链，则添加思维链字段
          if (cot && exportOptions.includeCOT && cotField) {
            item[cotField] = cot;
          }

          // 如果需要包含标签
          if (includeLabels && labels && labels.length > 0) {
            item.label = labels.split(' ')[1];
          }

          // 如果需要包含文本块
          if (includeChunk && chunkId) {
            item.chunk = chunkId;
          }

          return item;
        });
      }

      // 处理不同的文件格式
      let content;
      let fileExtension;

      if (exportOptions.fileFormat === 'jsonl') {
        // JSONL 格式：每行一个 JSON 对象
        content = formattedData.map(item => JSON.stringify(item)).join('\n');
        fileExtension = 'jsonl';
      } else if (exportOptions.fileFormat === 'csv') {
        // CSV 格式
        const headers = Object.keys(formattedData[0] || {});
        const csvRows = [
          // 添加表头
          headers.join(','),
          // 添加数据行
          ...formattedData.map(item =>
            headers
              .map(header => {
                // 处理包含逗号、换行符或双引号的字段
                let field = item[header]?.toString() || '';
                if (exportOptions.formatType === 'sharegpt') field = JSON.stringify(item[header]);
                if (field.includes(',') || field.includes('\n') || field.includes('"')) {
                  field = `"${field.replace(/"/g, '""')}"`;
                }
                return field;
              })
              .join(',')
          )
        ];
        content = csvRows.join('\n');
        fileExtension = 'csv';
      } else {
        // 默认 JSON 格式
        content = JSON.stringify(formattedData, null, 2);
        fileExtension = 'json';
      }

      // 创建 Blob 对象
      const blob = new Blob([content], { type: mimeType || 'application/json' });

      // 创建下载链接
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      const formatSuffix = exportOptions.formatType === 'alpaca' ? 'alpaca' : 'sharegpt';
      a.download = `datasets-${projectId}-${formatSuffix}-${new Date().toISOString().slice(0, 10)}.${fileExtension}`;

      // 触发下载
      document.body.appendChild(a);
      a.click();

      // 清理
      document.body.removeChild(a);
      URL.revokeObjectURL(url);

      toast.success(t('datasets.exportSuccess'));
      return true;
    } catch (error) {
      toast.error(error.message);
      return false;
    }
  };

  return { exportDatasets };
};

export default useDatasetExport;

import { NextResponse } from 'next/server';
import { distillQuestionsPrompt } from '@/lib/llm/prompts/distillQuestions';
import { distillQuestionsEnPrompt } from '@/lib/llm/prompts/distillQuestionsEn';
import { db } from '@/lib/db';
import { getProject } from '@/lib/db/projects';

const LLMClient = require('@/lib/llm/core');

/**
 * 生成问题接口：根据某个标签链路构造指定数量的问题
 */
export async function POST(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: '项目ID不能为空' }, { status: 400 });
    }

    const { tagPath, currentTag, tagId, count = 5, model, language = 'zh' } = await request.json();

    if (!currentTag || !tagPath) {
      const errorMsg = language === 'en' ? 'Tag information cannot be empty' : '标签信息不能为空';
      return NextResponse.json({ error: errorMsg }, { status: 400 });
    }

    // 首先获取或创建蒸馏文本块
    let distillChunk = await db.chunks.findFirst({
      where: {
        projectId,
        name: 'Distilled Content'
      }
    });

    if (!distillChunk) {
      // 创建一个特殊的蒸馏文本块
      distillChunk = await db.chunks.create({
        data: {
          name: 'Distilled Content',
          projectId,
          fileId: 'distilled',
          fileName: 'distilled.md',
          content:
            'This text block is used to store questions generated through data distillation and is not related to actual literature.',
          summary: 'Questions generated through data distillation',
          size: 0
        }
      });
    }

    // 获取已有的问题，避免重复
    const existingQuestions = await db.questions.findMany({
      where: {
        projectId,
        label: currentTag,
        chunkId: distillChunk.id // 使用蒸馏文本块的 ID
      },
      select: { question: true }
    });

    const existingQuestionTexts = existingQuestions.map(q => q.question);

    // 创建LLM客户端
    // 使用前端传过来的模型配置
    const llmClient = new LLMClient(model);

    // 获取项目配置
    const project = await getProject(projectId);
    const { globalPrompt } = project || {};

    // 生成提示词
    const promptFunc = language === 'en' ? distillQuestionsEnPrompt : distillQuestionsPrompt;
    const prompt = promptFunc(tagPath, currentTag, count, existingQuestionTexts, globalPrompt);

    // 调用大模型生成问题
    const { answer } = await llmClient.getResponseWithCOT(prompt);

    // 解析返回的问题
    let questions = [];

    try {
      questions = JSON.parse(answer);
    } catch (error) {
      console.error('解析问题JSON失败:', String(error));
      // 尝试使用正则表达式提取问题
      const matches = answer.match(/"([^"]+)"/g);
      if (matches) {
        questions = matches.map(match => match.replace(/"/g, ''));
      }
    }

    // 保存问题到数据库
    const savedQuestions = [];
    for (const questionText of questions) {
      const question = await db.questions.create({
        data: {
          question: questionText,
          projectId,
          label: currentTag,
          chunkId: distillChunk.id
        }
      });
      savedQuestions.push(question);
    }

    return NextResponse.json(savedQuestions);
  } catch (error) {
    console.error('生成问题失败:', String(error));
    return NextResponse.json({ error: error.message || '生成问题失败' }, { status: 500 });
  }
}

import { createOllama } from 'ollama-ai-provider';
import BaseClient from './base.js';

class OllamaClient extends BaseClient {
  constructor(config) {
    super(config);
    this.ollama = createOllama({
      baseURL: this.endpoint,
      apiKey: this.apiKey
    });
  }

  _getModel() {
    return this.ollama(this.model);
  }

  /**
   * 获取本地可用的模型列表
   * @returns {Promise<Array>} 返回模型列表
   */
  async getModels() {
    try {
      const response = await fetch(this.endpoint + '/tags');
      const data = await response.json();
      // 处理响应，提取模型名称
      if (data && data.models) {
        return data.models.map(model => ({
          name: model.name,
          modified_at: model.modified_at,
          size: model.size
        }));
      }
      return [];
    } catch (error) {
      console.error('Fetch error:', error);
    }
  }

  async chatStreamAPI(messages, options) {
    const model = this._getModel();
    const modelName = typeof model === 'function' ? model.modelName : this.model;

    // 构建符合 Ollama API 的请求数据
    const payload = {
      model: modelName,
      messages: this._convertJson(messages),
      stream: true, // 开启流式输出
      options: {
        temperature: options.temperature || this.modelConfig.temperature,
        top_p: options.top_p || this.modelConfig.top_p,
        num_predict: options.max_tokens || this.modelConfig.max_tokens
      }
    };

    if (this.endpoint.endsWith('/api')) {
      this.endpoint = this.endpoint.slice(0, -4);
    }

    try {
      // 发起流式请求
      const response = await fetch(`${this.endpoint.endsWith('/') ? this.endpoint : `${this.endpoint}/`}api/chat`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(payload)
      });

      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`API请求失败: ${response.status} ${response.statusText}\n${errorText}`);
      }

      if (!response.body) {
        throw new Error('响应中没有可读取的数据流');
      }

      // 处理原始数据流，实现思维链的流式输出
      const reader = response.body.getReader();
      const encoder = new TextEncoder();
      const decoder = new TextDecoder();

      // 创建一个新的可读流
      const newStream = new ReadableStream({
        async start(controller) {
          let buffer = '';
          let isThinking = false; // 当前是否在输出思维链模式
          let pendingReasoning = null; // 等待输出的思维链

          // 输出文本内容
          const sendContent = text => {
            if (!text) return;

            // 如果正在输出思维链，需要先关闭思维链标签
            if (isThinking) {
              controller.enqueue(encoder.encode('</think>'));
              isThinking = false;
            }

            controller.enqueue(encoder.encode(text));
          };

          // 流式输出思维链
          const sendReasoning = text => {
            if (!text) return;

            // 如果还没有开始思维链输出，需要先添加思维链标签
            if (!isThinking) {
              controller.enqueue(encoder.encode('<think>'));
              isThinking = true;
            }

            controller.enqueue(encoder.encode(text));
          };

          try {
            while (true) {
              const { done, value } = await reader.read();

              if (done) {
                // 流结束时，如果还在思维链模式，关闭标签
                if (isThinking) {
                  controller.enqueue(encoder.encode('</think>'));
                }
                controller.close();
                break;
              }

              // 解析数据块
              const chunk = decoder.decode(value, { stream: true });
              buffer += chunk;

              // 处理数据行
              let boundary = buffer.indexOf('\n');
              while (boundary !== -1) {
                const line = buffer.substring(0, boundary).trim();
                buffer = buffer.substring(boundary + 1);

                if (line) {
                  try {
                    // 解析JSON数据
                    const jsonData = JSON.parse(line);
                    const deltaContent = jsonData.message?.content;
                    const deltaReasoning = jsonData.message?.thinking;

                    // 如果有思维链内容，则实时流式输出
                    if (deltaReasoning) {
                      sendReasoning(deltaReasoning);
                    }

                    // 如果有正文内容也实时输出
                    if (deltaContent !== undefined && deltaContent !== null) {
                      sendContent(deltaContent);
                    }
                  } catch (e) {
                    // 忽略 JSON 解析错误
                    console.error('解析响应数据出错:', e);
                  }
                }

                boundary = buffer.indexOf('\n');
              }
            }
          } catch (error) {
            console.error('处理数据流时出错:', error);
            // 如果出错时正在输出思维链，要关闭思维链标签
            if (isThinking) {
              try {
                controller.enqueue(encoder.encode('</think>'));
              } catch (e) {
                console.error('关闭思维链标签出错:', e);
              }
            }
            controller.error(error);
          }
        }
      });

      // 最终返回响应流
      return new Response(newStream, {
        headers: {
          'Content-Type': 'text/plain', // 纯文本格式
          'Cache-Control': 'no-cache',
          Connection: 'keep-alive'
        }
      });
    } catch (error) {
      console.error('流式API调用出错:', error);
      throw error;
    }
  }
}

module.exports = OllamaClient;

const fs = require('fs');
const path = require('path');

/**
 * 设置应用日志系统
 * @param {Object} app Electron app 对象
 * @returns {string} 日志文件路径
 */
function setupLogging(app) {
  const logDir = path.join(app.getPath('userData'), 'logs');
  if (!fs.existsSync(logDir)) {
    fs.mkdirSync(logDir, { recursive: true });
  }

  const logFilePath = path.join(logDir, `app-${new Date().toISOString().slice(0, 10)}.log`);

  // 创建自定义日志函数
  global.appLog = (message, level = 'info') => {
    const timestamp = new Date().toISOString();
    const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;

    // 同时输出到控制台和日志文件
    console.log(message);
    fs.appendFileSync(logFilePath, logEntry);
  };

  // 捕获全局未处理异常并记录
  process.on('uncaughtException', error => {
    global.appLog(`未捕获的异常: ${error.stack || error}`, 'error');
  });

  return logFilePath;
}

/**
 * 设置 IPC 日志处理程序
 * @param {Object} ipcMain IPC 主进程对象
 * @param {Object} app Electron app 对象
 * @param {boolean} isDev 是否为开发环境
 */
function setupIpcLogging(ipcMain, app, isDev) {
  ipcMain.on('log', (event, { level, message }) => {
    const timestamp = new Date().toISOString();
    const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;

    // 只在客户端环境下写入文件
    if (!isDev || true) {
      const logsDir = path.join(app.getPath('userData'), 'logs');
      if (!fs.existsSync(logsDir)) {
        fs.mkdirSync(logsDir, { recursive: true });
      }
      const logFile = path.join(logsDir, `${new Date().toISOString().split('T')[0]}.log`);
      fs.appendFileSync(logFile, logEntry);
    }

    // 同时输出到控制台
    console[level](message);
  });
}

/**
 * 清理日志文件
 * @param {Object} app Electron app 对象
 * @returns {Promise<void>}
 */
async function clearLogs(app) {
  const logsDir = path.join(app.getPath('userData'), 'logs');
  if (fs.existsSync(logsDir)) {
    // 读取目录下所有文件
    const files = await fs.promises.readdir(logsDir);
    // 删除所有文件
    for (const file of files) {
      const filePath = path.join(logsDir, file);
      await fs.promises.unlink(filePath);
      global.appLog(`已删除日志文件: ${filePath}`);
    }
  }
}

module.exports = {
  setupLogging,
  setupIpcLogging,
  clearLogs
};

import pdf2md from '@opendocsg/pdf2md';
import { getProjectRoot } from '@/lib/db/base';
import fs from 'fs';
import path from 'path';

export async function defaultProcessing(projectId, fileName) {
  console.log('executing default pdf conversion strategy......');

  // 获取项目根目录
  const projectRoot = await getProjectRoot();
  const projectPath = path.join(projectRoot, projectId);

  // 获取文件路径
  const filePath = path.join(projectPath, 'files', fileName);

  // 读取文件
  const pdfBuffer = fs.readFileSync(filePath);

  // 转换后文件名
  const convertName = fileName.replace(/\.([^.]*)$/, '') + '.md';

  try {
    const text = await pdf2md(pdfBuffer);
    const outputFile = path.join(projectPath, 'files', convertName);
    console.log(`Writing to ${outputFile}...`);
    fs.writeFileSync(path.resolve(outputFile), text);
    console.log('pdf conversion completed!');

    // 返回转换后的文件名
    return { success: true, fileName: convertName };
  } catch (err) {
    console.error('pdf conversion failed:', err);
    throw err;
  }
}

export default {
  defaultProcessing
};

'use client';

import { useState, useEffect } from 'react';
import { Container, Typography, Box, Tabs, Tab, Paper, Alert, CircularProgress } from '@mui/material';
import { useSearchParams, useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';

// 导入设置组件
import BasicSettings from '@/components/settings/BasicSettings';
import ModelSettings from '@/components/settings/ModelSettings';
import TaskSettings from '@/components/settings/TaskSettings';
import PromptSettings from './components/PromptSettings';

// 定义 TAB 枚举
const TABS = {
  BASIC: 'basic',
  MODEL: 'model',
  TASK: 'task',
  PROMPTS: 'prompts'
};

export default function SettingsPage({ params }) {
  const { t } = useTranslation();
  const { projectId } = params;
  const searchParams = useSearchParams();
  const router = useRouter();
  const [activeTab, setActiveTab] = useState(TABS.BASIC);
  const [projectExists, setProjectExists] = useState(true);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // 从 URL hash 中获取当前 tab
  useEffect(() => {
    const tab = searchParams.get('tab');
    if (tab && Object.values(TABS).includes(tab)) {
      setActiveTab(tab);
    }
  }, [searchParams]);

  // 检查项目是否存在
  useEffect(() => {
    async function checkProject() {
      try {
        setLoading(true);
        const response = await fetch(`/api/projects/${projectId}`);

        if (!response.ok) {
          if (response.status === 404) {
            setProjectExists(false);
          } else {
            throw new Error(t('projects.fetchFailed'));
          }
        } else {
          setProjectExists(true);
        }
      } catch (error) {
        console.error('获取项目详情出错:', error);
        setError(error.message);
      } finally {
        setLoading(false);
      }
    }

    checkProject();
  }, [projectId, t]);

  // 处理 tab 切换
  const handleTabChange = (event, newValue) => {
    setActiveTab(newValue);
    // 更新 URL hash
    router.push(`/projects/${projectId}/settings?tab=${newValue}`);
  };

  if (loading) {
    return (
      <Container maxWidth="lg" sx={{ mt: 4, display: 'flex', justifyContent: 'center' }}>
        <CircularProgress />
      </Container>
    );
  }

  if (!projectExists) {
    return (
      <Container maxWidth="lg" sx={{ mt: 4 }}>
        <Alert severity="error">{t('projects.notExist')}</Alert>
      </Container>
    );
  }

  if (error) {
    return (
      <Container maxWidth="lg" sx={{ mt: 4 }}>
        <Alert severity="error">{error}</Alert>
      </Container>
    );
  }

  return (
    <Container maxWidth="lg" sx={{ mt: 4 }}>
      <Paper sx={{ mb: 4 }}>
        <Tabs
          value={activeTab}
          onChange={handleTabChange}
          variant="fullWidth"
          textColor="primary"
          indicatorColor="primary"
          aria-label={t('settings.tabsAriaLabel')}
        >
          <Tab value={TABS.BASIC} label={t('settings.basicInfo')} />
          <Tab value={TABS.MODEL} label={t('settings.modelConfig')} />
          <Tab value={TABS.TASK} label={t('settings.taskConfig')} />
          <Tab value={TABS.PROMPTS} label={t('settings.promptConfig')} />
        </Tabs>
      </Paper>

      {activeTab === TABS.BASIC && <BasicSettings projectId={projectId} />}

      {activeTab === TABS.MODEL && <ModelSettings projectId={projectId} />}

      {activeTab === TABS.TASK && <TaskSettings projectId={projectId} />}

      {activeTab === TABS.PROMPTS && <PromptSettings projectId={projectId} />}
    </Container>
  );
}

const fs = require('fs');
const path = require('path');
const { PrismaClient } = require('@prisma/client');

/**
 * 执行SQL命令
 * @param {string} dbUrl 数据库连接 URL
 * @param {string} sql SQL命令
 * @returns {Promise<void>}
 */
async function executeSql(dbUrl, sql) {
  // 允许多条SQL语句分开执行，支持分号和空行分隔
  const statements = sql
    .split(';')
    .map(stmt => stmt.trim())
    .filter(stmt => stmt.length > 0);

  if (statements.length === 0) {
    return;
  }

  // 设置环境变量
  process.env.DATABASE_URL = dbUrl;

  // 创建Prisma实例
  const prisma = new PrismaClient();

  try {
    // 执行每条SQL语句
    for (const statement of statements) {
      await prisma.$executeRawUnsafe(statement);
    }
  } finally {
    // 关闭连接
    await prisma.$disconnect();
  }
}

/**
 * 获取本地和应用的SQL配置文件
 * @param {string} userDataPath 用户数据目录
 * @param {string} resourcesPath 应用资源目录
 * @param {boolean} isDev 是否开发环境
 * @returns {Promise<{userSqlConfig: Array, appSqlConfig: Array}>}
 */
async function getSqlConfigs(userDataPath, resourcesPath, isDev) {
  // 用户SQL配置文件路径
  const userSqlPath = path.join(userDataPath, 'sql.json');

  // 应用SQL配置文件路径
  const appSqlPath = isDev
    ? path.join(__dirname, '..', 'prisma', 'sql.json')
    : path.join(resourcesPath, 'prisma', 'sql.json');

  let userSqlConfig = [];
  let appSqlConfig = [];

  // 读取应用SQL配置
  try {
    if (fs.existsSync(appSqlPath)) {
      const appSqlContent = fs.readFileSync(appSqlPath, 'utf8');
      appSqlConfig = JSON.parse(appSqlContent);
    }
  } catch (error) {
    throw new Error(`读取应用SQL配置文件失败: ${error.message}`);
  }

  // 读取用户SQL配置（如果存在）
  try {
    if (fs.existsSync(userSqlPath)) {
      const userSqlContent = fs.readFileSync(userSqlPath, 'utf8');
      userSqlConfig = JSON.parse(userSqlContent);
    }
  } catch (error) {
    // 如果用户SQL配置不存在或无法解析，使用空数组
    userSqlConfig = [];
  }

  return { userSqlConfig, appSqlConfig };
}

/**
 * 更新用户SQL配置文件
 * @param {string} userDataPath 用户数据目录
 * @param {Array} sqlConfig 新的SQL配置
 */
function updateUserSqlConfig(userDataPath, sqlConfig) {
  const userSqlPath = path.join(userDataPath, 'sql.json');
  fs.writeFileSync(userSqlPath, JSON.stringify(sqlConfig, null, 4), 'utf8');
}

// 不再需要版本比较功能

/**
 * 获取需要执行的SQL命令
 * @param {Array} userSqlConfig 用户SQL配置
 * @param {Array} appSqlConfig 应用SQL配置
 * @returns {Array} 需要执行的SQL命令
 */
function getSqlsToExecute(userSqlConfig, appSqlConfig) {
  // 创建用户已执行的SQL集合 (使用 version + sql 的组合作为唯一标识)
  const userExecutedSqlSet = new Set();
  userSqlConfig.forEach(item => {
    const key = `${item.version}:${item.sql}`;
    userExecutedSqlSet.add(key);
  });

  // 过滤出用户需要执行的SQL (即应用SQL配置中存在但用户尚未执行的SQL)
  return appSqlConfig.filter(item => {
    const key = `${item.version}:${item.sql}`;
    return !userExecutedSqlSet.has(key);
  });
}

/**
 * 更新数据库
 * @param {string} userDataPath 用户数据目录
 * @param {string} resourcesPath 应用资源目录
 * @param {boolean} isDev 是否开发环境
 * @param {function} logger 日志函数
 */
async function updateDatabase(userDataPath, resourcesPath, isDev, logger = console.log) {
  const dbPath = path.join(userDataPath, 'local-db', 'db.sqlite');

  try {
    // 获取SQL配置
    const { userSqlConfig, appSqlConfig } = await getSqlConfigs(userDataPath, resourcesPath, isDev);

    // 获取需要执行的SQL
    const sqlsToExecute = getSqlsToExecute(userSqlConfig, appSqlConfig);

    if (sqlsToExecute.length === 0) {
      logger('数据库已是最新版本，无需更新');
      return { updated: false, message: '数据库已是最新版本' };
    }

    // 设置数据库URL
    const dbUrl = `file:${dbPath}`;

    // 执行SQL更新
    logger(`发现 ${sqlsToExecute.length} 个数据库更新，开始执行...`);
    for (const item of sqlsToExecute) {
      try {
        logger(`执行版本 ${item.version} 的SQL更新: ${item.sql.substring(0, 100)}...`);
        await executeSql(dbUrl, item.sql);
        // 添加到用户SQL配置
        userSqlConfig.push(item);
      } catch (error) {
        logger(`执行版本 ${item.version} 的SQL更新失败: ${error.message}`);
      }
    }

    // 更新用户SQL配置文件
    updateUserSqlConfig(userDataPath, userSqlConfig);

    logger('数据库更新完成');
    return {
      updated: true,
      message: `成功执行了 ${sqlsToExecute.length} 个数据库更新`,
      executedVersions: sqlsToExecute.map(item => item.version)
    };
  } catch (error) {
    logger(`数据库更新失败: ${error.message}`);
    return { updated: false, error: error.message };
  }
}

module.exports = {
  updateDatabase,
  executeSql,
  getSqlConfigs,
  updateUserSqlConfig,
  getSqlsToExecute
};

'use client';

import {
  Card,
  Box,
  CardActionArea,
  CardContent,
  Typography,
  Avatar,
  Chip,
  Divider,
  IconButton,
  Tooltip
} from '@mui/material';
import Link from 'next/link';
import { styles } from '@/styles/home';
import DataObjectIcon from '@mui/icons-material/DataObject';
import DeleteIcon from '@mui/icons-material/Delete';
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
import VisibilityIcon from '@mui/icons-material/Visibility';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';

/**
 * 项目卡片组件
 * @param {Object} props - 组件属性
 * @param {Object} props.project - 项目数据
 * @param {Function} props.onDeleteClick - 删除按钮点击事件处理函数
 */
export default function ProjectCard({ project, onDeleteClick }) {
  const { t } = useTranslation();
  const [processingId, setProcessingId] = useState(false);

  // 打开项目目录
  const handleOpenDirectory = async event => {
    event.stopPropagation();
    event.preventDefault();

    if (processingId) return;

    try {
      setProcessingId(true);

      const response = await fetch('/api/projects/open-directory', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ projectId: project.id })
      });

      if (!response.ok) {
        const data = await response.json();
        throw new Error(data.error || t('migration.openDirectoryFailed'));
      }

      // 成功打开目录，不需要特别处理
    } catch (error) {
      console.error('打开目录错误:', error);
      alert(error.message);
    } finally {
      setProcessingId(false);
    }
  };

  // 处理删除按钮点击
  const handleDeleteClick = event => {
    event.stopPropagation();
    event.preventDefault();
    onDeleteClick(event, project);
  };

  return (
    <Card sx={styles.projectCard}>
      <Link href={`/projects/${project.id}`} passHref style={{ textDecoration: 'none', color: 'inherit' }}>
        <CardActionArea component="div">
          <CardContent sx={{ pt: 2 }}>
            <Box
              sx={{
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'flex-start',
                mb: 0.5
              }}
            >
              <Typography variant="h5" component="div" fontWeight="600" sx={{ mt: 1 }}>
                {project.name}
              </Typography>
              <Box sx={{ display: 'flex', gap: 1 }}>
                <Chip
                  size="small"
                  label={`${project._count.Questions || 0} ${t('projects.questions')}`}
                  color="primary"
                  variant="outlined"
                />
                <Chip
                  size="small"
                  label={`${project._count.Datasets || 0} ${t('projects.datasets')}`}
                  color="secondary"
                  variant="outlined"
                />
              </Box>
            </Box>

            <Typography variant="body2" color="text.secondary" sx={styles.projectDescription}>
              {project.description}
            </Typography>

            <Divider sx={{ mb: 2 }} />

            <Box
              sx={{
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'center'
              }}
            >
              <Typography variant="caption" color="text.secondary">
                {t('projects.lastUpdated')}: {new Date(project.updateAt).toLocaleDateString('zh-CN')}
              </Typography>
              <Box sx={{ display: 'flex', alignItems: 'center' }}>
                <Tooltip title={t('projects.viewDetails')}>
                  <IconButton size="small" color="primary" sx={{ mr: 1 }}>
                    <VisibilityIcon fontSize="small" />
                  </IconButton>
                </Tooltip>
                <Tooltip title={t('projects.openDirectory')}>
                  <IconButton
                    size="small"
                    color="primary"
                    onClick={handleOpenDirectory}
                    disabled={processingId}
                    sx={{ mr: 1 }}
                  >
                    <FolderOpenIcon fontSize="small" />
                  </IconButton>
                </Tooltip>
                <IconButton size="small" color="error" onClick={handleDeleteClick}>
                  <DeleteIcon fontSize="small" />
                </IconButton>
              </Box>
            </Box>
          </CardContent>
        </CardActionArea>
      </Link>
    </Card>
  );
}

import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// 获取项目的所有任务列表
export async function GET(request, { params }) {
  try {
    const { projectId } = params;
    const { searchParams } = new URL(request.url);

    // 可选参数: 任务类型和任务状态
    const taskType = searchParams.get('taskType');
    const statusStr = searchParams.get('status');

    // 分页参数
    const page = parseInt(searchParams.get('page') || '0');
    const limit = parseInt(searchParams.get('limit') || '10');

    // 构建查询条件
    const where = { projectId };

    if (taskType) {
      where.taskType = taskType;
    }

    if (statusStr && !isNaN(parseInt(statusStr))) {
      where.status = parseInt(statusStr);
    }

    // 获取任务总数
    const total = await prisma.task.count({ where });

    // 获取任务列表，按创建时间降序排序，并应用分页
    const tasks = await prisma.task.findMany({
      where,
      orderBy: {
        createAt: 'desc'
      },
      skip: page * limit,
      take: limit
    });

    return NextResponse.json({
      code: 0,
      data: tasks,
      total,
      page,
      limit,
      message: '任务列表获取成功'
    });
  } catch (error) {
    console.error('获取任务列表失败:', String(error));
    return NextResponse.json(
      {
        code: 500,
        error: '获取任务列表失败',
        message: error.message
      },
      { status: 500 }
    );
  }
}

'use client';

import { useState, useEffect } from 'react';
import {
  Typography,
  Box,
  Button,
  TextField,
  Grid,
  Card,
  CardContent,
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  FormControl,
  Autocomplete,
  Slider,
  InputLabel,
  Select,
  MenuItem,
  Stack,
  Paper,
  Avatar,
  Tooltip,
  IconButton,
  Chip
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import ErrorIcon from '@mui/icons-material/Error';
import { DEFAULT_MODEL_SETTINGS, MODEL_PROVIDERS } from '@/constant/model';
import { useTranslation } from 'react-i18next';
import axios from 'axios';
import { ProviderIcon } from '@lobehub/icons';
import { toast } from 'sonner';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import ScienceIcon from '@mui/icons-material/Science';
import { useRouter } from 'next/navigation';
import { useAtom } from 'jotai';
import { modelConfigListAtom, selectedModelInfoAtom } from '@/lib/store';

export default function ModelSettings({ projectId }) {
  const { t } = useTranslation();
  const router = useRouter();
  // 模型对话框状态
  const [openModelDialog, setOpenModelDialog] = useState(false);
  const [editingModel, setEditingModel] = useState(null);
  const [loading, setLoading] = useState(true);
  const [providerList, setProviderList] = useState([]);
  const [providerOptions, setProviderOptions] = useState([]);
  const [selectedProvider, setSelectedProvider] = useState({});
  const [models, setModels] = useState([]);
  const [modelConfigList, setModelConfigList] = useAtom(modelConfigListAtom);
  const [selectedModelInfo, setSelectedModelInfo] = useAtom(selectedModelInfoAtom);
  const [modelConfigForm, setModelConfigForm] = useState({
    id: '',
    providerId: '',
    providerName: '',
    endpoint: '',
    apiKey: '',
    modelId: '',
    modelName: '',
    type: 'text',
    temperature: 0.0,
    maxTokens: 0,
    topP: 0,
    topK: 0,
    status: 1
  });

  useEffect(() => {
    getProvidersList();
    getModelConfigList();
  }, []);

  // 获取提供商列表
  const getProvidersList = () => {
    axios.get('/api/llm/providers').then(response => {
      console.log('获取的模型列表:', response.data);
      setProviderList(response.data);
      const providerOptions = response.data.map(provider => ({
        id: provider.id,
        label: provider.name
      }));
      setSelectedProvider(response.data[0]);
      getProviderModels(response.data[0].id);
      setProviderOptions(providerOptions);
    });
  };

  // 获取模型配置列表
  const getModelConfigList = () => {
    axios
      .get(`/api/projects/${projectId}/model-config`)
      .then(response => {
        setModelConfigList(response.data.data);
        setLoading(false);
      })
      .catch(error => {
        setLoading(false);
        toast.error('Fetch model list Error', { duration: 3000 });
      });
  };

  const onChangeProvider = (event, newValue) => {
    console.log('选择提供商:', newValue, typeof newValue);
    if (typeof newValue === 'string') {
      // 用户手动输入了自定义提供商
      setModelConfigForm(prev => ({
        ...prev,
        providerId: 'custom',
        endpoint: '',
        providerName: ''
      }));
    } else if (newValue && newValue.id) {
      // 用户从下拉列表中选择了一个提供商
      const selectedProvider = providerList.find(p => p.id === newValue.id);
      if (selectedProvider) {
        setSelectedProvider(selectedProvider);
        setModelConfigForm(prev => ({
          ...prev,
          providerId: selectedProvider.id,
          endpoint: selectedProvider.apiUrl,
          providerName: selectedProvider.name,
          modelName: ''
        }));
        getProviderModels(newValue.id);
      }
    }
  };

  // 获取提供商的模型列表（DB）
  const getProviderModels = providerId => {
    axios
      .get(`/api/llm/model?providerId=${providerId}`)
      .then(response => {
        setModels(response.data);
      })
      .catch(error => {
        toast.error('Get Models Error', { duration: 3000 });
      });
  };

  //同步模型列表
  const refreshProviderModels = async () => {
    let data = await getNewModels();
    if (!data) return;
    if (data.length > 0) {
      setModels(data);
      toast.success('Refresh Success', { duration: 3000 });
      const newModelsData = await axios.post('/api/llm/model', {
        newModels: data,
        providerId: selectedProvider.id
      });
      if (newModelsData.status === 200) {
        toast.success('Get Model Success', { duration: 3000 });
      }
    } else {
      toast.info('No Models Need Refresh', { duration: 3000 });
    }
  };

  //获取最新模型列表
  async function getNewModels() {
    try {
      if (!modelConfigForm || !modelConfigForm.endpoint) {
        return null;
      }
      const providerId = modelConfigForm.providerId;
      console.log(providerId, 'getNewModels providerId');

      // 使用后端 API 代理请求
      const res = await axios.post('/api/llm/fetch-models', {
        endpoint: modelConfigForm.endpoint,
        providerId: providerId,
        apiKey: modelConfigForm.apiKey
      });

      return res.data;
    } catch (err) {
      if (err.response && err.response.status === 401) {
        toast.error('API Key Invalid', { duration: 3000 });
      } else {
        toast.error('Get Model List Error', { duration: 3000 });
      }
      return null;
    }
  }

  // 打开模型对话框
  const handleOpenModelDialog = (model = null) => {
    if (model) {
      console.log('handleOpenModelDialog', model);
      setModelConfigForm(model);
      getProviderModels(model.providerId);
    } else {
      setModelConfigForm({
        ...modelConfigForm,
        apiKey: '',
        ...DEFAULT_MODEL_SETTINGS,
        id: ''
      });
    }
    setOpenModelDialog(true);
  };

  // 关闭模型对话框
  const handleCloseModelDialog = () => {
    setOpenModelDialog(false);
  };

  // 处理模型表单变更
  const handleModelFormChange = e => {
    const { name, value } = e.target;
    console.log('handleModelFormChange', name, value);
    setModelConfigForm(prev => ({
      ...prev,
      [name]: value
    }));
  };

  // 保存模型
  const handleSaveModel = () => {
    axios
      .post(`/api/projects/${projectId}/model-config`, modelConfigForm)
      .then(response => {
        if (selectedModelInfo && selectedModelInfo.id === response.data.id) {
          setSelectedModelInfo(response.data);
        }
        toast.success(t('settings.saveSuccess'), { duration: 3000 });
        getModelConfigList();
        handleCloseModelDialog();
      })
      .catch(error => {
        toast.error(t('settings.saveFailed'));
        console.error(error);
      });
  };

  // 删除模型
  const handleDeleteModel = id => {
    axios
      .delete(`/api/projects/${projectId}/model-config/${id}`)
      .then(response => {
        toast.success(t('settings.deleteSuccess'), { duration: 3000 });
        getModelConfigList();
      })
      .catch(error => {
        toast.error(t('settings.deleteFailed'), { duration: 3000 });
      });
  };

  // 获取模型状态图标和颜色
  const getModelStatusInfo = model => {
    if (model.providerId.toLowerCase() === 'ollama') {
      return {
        icon: <CheckCircleIcon fontSize="small" />,
        color: 'success',
        text: t('models.localModel')
      };
    } else if (model.apiKey) {
      return {
        icon: <CheckCircleIcon fontSize="small" />,
        color: 'success',
        text: t('models.apiKeyConfigured')
      };
    } else {
      return {
        icon: <ErrorIcon fontSize="small" />,
        color: 'warning',
        text: t('models.apiKeyNotConfigured')
      };
    }
  };

  if (loading) {
    return <Typography>{t('textSplit.loading')}</Typography>;
  }

  return (
    <Card>
      <CardContent>
        <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
          <Typography variant="h6" fontWeight="bold"></Typography>
          <Box sx={{ display: 'flex', gap: 1 }}>
            <Button
              variant="outlined"
              color="secondary"
              startIcon={<ScienceIcon />}
              onClick={() => router.push(`/projects/${projectId}/playground`)}
              size="small"
              sx={{ textTransform: 'none' }}
            >
              {t('playground.title')}
            </Button>
            <Button
              variant="contained"
              color="primary"
              startIcon={<AddIcon />}
              onClick={() => handleOpenModelDialog()}
              size="small"
              sx={{ textTransform: 'none' }}
            >
              {t('models.add')}
            </Button>
          </Box>
        </Box>

        <Stack spacing={2}>
          {modelConfigList.map(model => (
            <Paper
              key={model.id}
              elevation={1}
              sx={{
                p: 2,
                borderRadius: 2,
                transition: 'all 0.2s',
                '&:hover': {
                  boxShadow: 3,
                  transform: 'translateY(-2px)'
                }
              }}
            >
              <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
                  <ProviderIcon key={model.providerId} provider={model.providerId} size={32} type={'color'} />
                  <Box>
                    <Typography variant="subtitle1" fontWeight="bold">
                      {model.modelName ? model.modelName : t('models.unselectedModel')}
                    </Typography>
                    <Typography
                      variant="body2"
                      color="primary" // 改为主色调
                      sx={{
                        fontWeight: 'medium', // 加粗
                        bgcolor: 'primary.50', // 添加背景色
                        px: 1, // 水平内边距
                        py: 0.2, // 垂直内边距
                        borderRadius: 1, // 圆角
                        display: 'inline-block' // 行内块元素
                      }}
                    >
                      {model.providerName}
                    </Typography>
                  </Box>
                </Box>

                <Box sx={{ display: 'flex', gap: 1 }}>
                  <Tooltip title={getModelStatusInfo(model).text}>
                    <Chip
                      icon={getModelStatusInfo(model).icon}
                      label={
                        model.endpoint.replace(/^https?:\/\//, '') +
                        (model.providerId.toLowerCase() !== 'ollama' && !model.apiKey
                          ? ' (' + t('models.unconfiguredAPIKey') + ')'
                          : '')
                      }
                      size="small"
                      color={getModelStatusInfo(model).color}
                      variant="outlined"
                    />
                  </Tooltip>
                  <Tooltip title={t('models.typeTips')}>
                    <Chip
                      sx={{ marginLeft: '5px' }}
                      label={t(`models.${model.type || 'text'}`)}
                      size="small"
                      color={model.type === 'vision' ? 'secondary' : 'info'}
                      variant="outlined"
                    />
                  </Tooltip>
                  <Tooltip title={t('playground.title')}>
                    <IconButton
                      size="small"
                      onClick={() => router.push(`/projects/${projectId}/playground?modelId=${model.id}`)}
                      color="secondary"
                    >
                      <ScienceIcon fontSize="small" />
                    </IconButton>
                  </Tooltip>

                  <Tooltip title={t('common.edit')}>
                    <IconButton size="small" onClick={() => handleOpenModelDialog(model)} color="primary">
                      <EditIcon fontSize="small" />
                    </IconButton>
                  </Tooltip>

                  <Tooltip title={t('common.delete')}>
                    <IconButton
                      size="small"
                      onClick={() => handleDeleteModel(model.id)}
                      disabled={modelConfigList.length <= 1}
                      color="error"
                    >
                      <DeleteIcon fontSize="small" />
                    </IconButton>
                  </Tooltip>
                </Box>
              </Box>
            </Paper>
          ))}
        </Stack>
      </CardContent>

      {/* 模型表单对话框 */}
      <Dialog open={openModelDialog} onClose={handleCloseModelDialog} maxWidth="sm" fullWidth>
        <DialogTitle>{editingModel ? t('models.edit') : t('models.add')}</DialogTitle>
        <DialogContent>
          <Grid container spacing={2} sx={{ mt: 0.5 }}>
            {/*ai提供商*/}
            <Grid item xs={12}>
              <FormControl fullWidth>
                <Autocomplete
                  freeSolo
                  options={providerOptions}
                  getOptionLabel={option => option.label}
                  value={
                    providerOptions.find(p => p.id === modelConfigForm.providerId) || {
                      id: 'custom',
                      label: modelConfigForm.providerName || ''
                    }
                  }
                  onChange={onChangeProvider}
                  renderInput={params => (
                    <TextField
                      {...params}
                      label={t('models.provider')}
                      onChange={e => {
                        // 当用户手动输入时，更新 provider 字段
                        setModelConfigForm(prev => ({
                          ...prev,
                          providerId: 'custom',
                          providerName: e.target.value
                        }));
                      }}
                    />
                  )}
                  renderOption={(props, option) => {
                    return (
                      <div {...props}>
                        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
                          <ProviderIcon key={option.id} provider={option.id} size={32} type={'color'} />
                          {option.label}
                        </div>
                      </div>
                    );
                  }}
                />
              </FormControl>
            </Grid>
            {/*接口地址*/}
            <Grid item xs={12}>
              <TextField
                fullWidth
                label={t('models.endpoint')}
                name="endpoint"
                value={modelConfigForm.endpoint}
                onChange={handleModelFormChange}
                placeholder="例如: https://api.openai.com/v1"
              />
            </Grid>
            {/*api密钥*/}
            <Grid item xs={12}>
              <TextField
                fullWidth
                label={t('models.apiKey')}
                name="apiKey"
                type="password"
                value={modelConfigForm.apiKey}
                onChange={handleModelFormChange}
                placeholder="例如: sk-..."
              />
            </Grid>
            {/*模型列表*/}
            <Grid item xs={12} style={{ display: 'flex', alignItems: 'center' }}>
              <FormControl style={{ width: '70%' }}>
                <Autocomplete
                  freeSolo
                  options={models
                    .filter(model => model && model.modelName)
                    .map(model => ({
                      label: model.modelName,
                      id: model.id,
                      modelId: model.modelId,
                      providerId: model.providerId
                    }))}
                  value={modelConfigForm.modelName}
                  onChange={(event, newValue) => {
                    console.log('newValue', newValue);
                    setModelConfigForm(prev => ({
                      ...prev,
                      modelName: newValue?.label,
                      modelId: newValue?.modelId ? newValue?.modelId : newValue?.label
                    }));
                  }}
                  renderInput={params => (
                    <TextField
                      {...params}
                      label={t('models.modelName')}
                      onChange={e => {
                        setModelConfigForm(prev => ({
                          ...prev,
                          modelName: e.target.value
                        }));
                      }}
                    />
                  )}
                />
              </FormControl>
              <Button variant="contained" onClick={() => refreshProviderModels()} sx={{ ml: 2 }}>
                {t('models.refresh')}
              </Button>
            </Grid>
            {/* 新增：视觉模型选择项 */}
            <Grid item xs={12}>
              <FormControl fullWidth>
                <InputLabel>{t('models.type')}</InputLabel>
                <Select
                  label={t('models.type')}
                  value={modelConfigForm.type || 'text'}
                  onChange={handleModelFormChange}
                  name="type"
                >
                  <MenuItem value="text">{t('models.text')}</MenuItem>
                  <MenuItem value="vision">{t('models.vision')}</MenuItem>
                </Select>
              </FormControl>
            </Grid>
            <Grid item xs={12}>
              <Typography id="question-generation-length-slider" gutterBottom>
                {t('models.temperature')}
              </Typography>

              <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
                <Slider
                  min={0}
                  max={2}
                  name="temperature"
                  value={modelConfigForm.temperature}
                  onChange={handleModelFormChange}
                  step={0.1}
                  valueLabelDisplay="auto"
                  aria-label="Temperature"
                  sx={{ flex: 1 }}
                />
                <Typography variant="body2" sx={{ minWidth: '40px' }}>
                  {modelConfigForm.temperature}
                </Typography>
              </Box>
            </Grid>
            <Grid item xs={12}>
              <Typography id="question-generation-length-slider" gutterBottom>
                {t('models.maxTokens')}
              </Typography>

              <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
                <Slider
                  min={1024}
                  max={16384}
                  name="maxTokens"
                  value={modelConfigForm.maxTokens}
                  onChange={handleModelFormChange}
                  step={1}
                  valueLabelDisplay="auto"
                  aria-label="maxTokens"
                  sx={{ flex: 1 }}
                />
                <Typography variant="body2" sx={{ minWidth: '40px' }}>
                  {modelConfigForm.maxTokens}
                </Typography>
              </Box>
            </Grid>
          </Grid>
        </DialogContent>
        <DialogActions>
          <Button onClick={handleCloseModelDialog}>{t('common.cancel')}</Button>
          <Button
            onClick={handleSaveModel}
            variant="contained"
            disabled={!modelConfigForm.providerId || !modelConfigForm.providerName || !modelConfigForm.endpoint}
          >
            {t('common.save')}
          </Button>
        </DialogActions>
      </Dialog>
    </Card>
  );
}

'use client';

import React, { useRef, useEffect } from 'react';
import { Box, Typography, Paper, Grid, CircularProgress } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import ChatMessage from './ChatMessage';
import { playgroundStyles } from '@/styles/playground';
import { useTranslation } from 'react-i18next';

const ChatArea = ({ selectedModels, conversations, loading, getModelName }) => {
  const theme = useTheme();
  const styles = playgroundStyles(theme);
  const { t } = useTranslation();

  // 为每个模型创建独立的引用
  const chatContainerRefs = {
    model1: useRef(null),
    model2: useRef(null),
    model3: useRef(null)
  };

  // 为每个模型的聊天容器自动滚动到底部
  useEffect(() => {
    Object.values(chatContainerRefs).forEach(ref => {
      if (ref.current) {
        ref.current.scrollTop = ref.current.scrollHeight;
      }
    });
  }, [conversations]);

  if (selectedModels.length === 0) {
    return (
      <Box sx={styles.emptyStateBox}>
        <Typography color="textSecondary">{t('playground.selectModelFirst')}</Typography>
      </Box>
    );
  }

  return (
    <Grid container spacing={2} sx={styles.chatContainer}>
      {selectedModels.map((modelId, index) => {
        const modelConversation = conversations[modelId] || [];
        const isLoading = loading[modelId];
        const refKey = `model${index + 1}`;

        return (
          <Grid
            item
            xs={12}
            md={selectedModels.length > 1 ? 12 / selectedModels.length : 12}
            key={modelId}
            style={{ maxHeight: 'calc(100vh - 300px)' }}
          >
            <Paper elevation={1} sx={styles.modelPaper}>
              <Box sx={styles.modelHeader}>
                <Typography variant="subtitle2">{getModelName(modelId)}</Typography>
                {isLoading && <CircularProgress size={16} sx={{ ml: 1 }} color="inherit" />}
              </Box>

              <Box ref={chatContainerRefs[refKey]} sx={styles.modelChatBox}>
                {modelConversation.length === 0 ? (
                  <Box sx={styles.emptyChatBox}>
                    <Typography color="textSecondary" variant="body2">
                      {t('playground.sendFirstMessage')}
                    </Typography>
                  </Box>
                ) : (
                  modelConversation.map((message, msgIndex) => (
                    <React.Fragment key={msgIndex}>
                      <ChatMessage message={message} modelName={null} />
                    </React.Fragment>
                  ))
                )}
              </Box>
            </Paper>
          </Grid>
        );
      })}
    </Grid>
  );
};

export default ChatArea;

'use client';

import { useState, useEffect } from 'react';
import {
  Typography,
  Box,
  Button,
  TextField,
  Grid,
  Card,
  CardContent,
  Slider,
  InputAdornment,
  Alert,
  Snackbar,
  FormControl,
  Select,
  InputLabel,
  MenuItem,
  Chip,
  FormHelperText
} from '@mui/material';
import { useTranslation } from 'react-i18next';

import SaveIcon from '@mui/icons-material/Save';
import useTaskSettings from '@/hooks/useTaskSettings';

export default function TaskSettings({ projectId }) {
  const { t } = useTranslation();
  const { taskSettings, setTaskSettings, loading, error, success, setSuccess } = useTaskSettings(projectId);
  // 处理设置变更
  const handleSettingChange = e => {
    const { name, value } = e.target;
    setTaskSettings(prev => ({
      ...prev,
      [name]: value
    }));
  };

  // 处理滑块变更
  const handleSliderChange = name => (event, newValue) => {
    setTaskSettings(prev => ({
      ...prev,
      [name]: newValue
    }));
  };

  // 保存任务配置
  const handleSaveTaskSettings = async () => {
    try {
      // 确保数组类型的数据被正确处理
      const settingsToSave = { ...taskSettings };

      // 确保递归分块的分隔符数组存在
      if (settingsToSave.splitType === 'recursive' && settingsToSave.separatorsInput) {
        if (!settingsToSave.separators || !Array.isArray(settingsToSave.separators)) {
          settingsToSave.separators = settingsToSave.separatorsInput.split(',').map(item => item.trim());
        }
      }

      console.log('Saving settings:', settingsToSave);

      const response = await fetch(`/api/projects/${projectId}/tasks`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(settingsToSave)
      });

      if (!response.ok) {
        throw new Error(t('settings.saveTasksFailed'));
      }

      setSuccess(true);
    } catch (error) {
      console.error('保存任务配置出错:', error);
      //setError(error.message);
    }
  };

  const handleCloseSnackbar = () => {
    setSuccess(false);
    //setError(null);
  };

  if (loading) {
    return <Typography>{t('common.loading')}</Typography>;
  }

  return (
    <Box sx={{ position: 'relative', pb: 8 }}>
      {' '}
      {/* 添加底部填充，为固定按钮留出空间 */}
      <Card style={{ marginBottom: 20 }}>
        <CardContent>
          <Grid container spacing={3}>
            <Grid item xs={12}>
              <Typography variant="subtitle1" gutterBottom>
                {t('settings.textSplitSettings')}
              </Typography>
              <Box sx={{ px: 2, py: 1 }}>
                {/* 分块策略选择 */}
                <FormControl fullWidth sx={{ mb: 3 }}>
                  <InputLabel id="split-type-label">{t('settings.splitType')}</InputLabel>
                  <Select
                    labelId="split-type-label"
                    value={taskSettings.splitType || 'markdown'}
                    label={t('settings.splitType')}
                    name="splitType"
                    onChange={handleSettingChange}
                  >
                    <MenuItem value="markdown">
                      <Box>
                        <Typography variant="subtitle2">{t('settings.splitTypeMarkdown')}</Typography>
                        <Typography variant="caption" sx={{ display: 'block', color: 'text.secondary' }}>
                          {t('settings.splitTypeMarkdownDesc')}
                        </Typography>
                      </Box>
                    </MenuItem>
                    <MenuItem value="recursive">
                      <Box>
                        <Typography variant="subtitle2">{t('settings.splitTypeRecursive')}</Typography>
                        <Typography variant="caption" sx={{ display: 'block', color: 'text.secondary' }}>
                          {t('settings.splitTypeRecursiveDesc')}
                        </Typography>
                      </Box>
                    </MenuItem>
                    <MenuItem value="text">
                      <Box>
                        <Typography variant="subtitle2">{t('settings.splitTypeText')}</Typography>
                        <Typography variant="caption" sx={{ display: 'block', color: 'text.secondary' }}>
                          {t('settings.splitTypeTextDesc')}
                        </Typography>
                      </Box>
                    </MenuItem>
                    <MenuItem value="token">
                      <Box>
                        <Typography variant="subtitle2">{t('settings.splitTypeToken')}</Typography>
                        <Typography variant="caption" sx={{ display: 'block', color: 'text.secondary' }}>
                          {t('settings.splitTypeTokenDesc')}
                        </Typography>
                      </Box>
                    </MenuItem>
                    <MenuItem value="code">
                      <Box>
                        <Typography variant="subtitle2">{t('settings.splitTypeCode')}</Typography>
                        <Typography variant="caption" sx={{ display: 'block', color: 'text.secondary' }}>
                          {t('settings.splitTypeCodeDesc')}
                        </Typography>
                      </Box>
                    </MenuItem>
                  </Select>
                </FormControl>

                {/* Markdown模式设置 */}
                {(!taskSettings.splitType || taskSettings.splitType === 'markdown') && (
                  <>
                    <Typography id="text-split-min-length-slider" gutterBottom>
                      {t('settings.minLength')}: {taskSettings.textSplitMinLength}
                    </Typography>
                    <Slider
                      value={taskSettings.textSplitMinLength || 1500}
                      onChange={handleSliderChange('textSplitMinLength')}
                      aria-labelledby="text-split-min-length-slider"
                      valueLabelDisplay="auto"
                      step={100}
                      marks
                      min={100}
                      max={5000}
                    />

                    <Typography id="text-split-max-length-slider" gutterBottom sx={{ mt: 3 }}>
                      {t('settings.maxLength')}: {taskSettings.textSplitMaxLength}
                    </Typography>
                    <Slider
                      value={taskSettings.textSplitMaxLength || 2000}
                      onChange={handleSliderChange('textSplitMaxLength')}
                      aria-labelledby="text-split-max-length-slider"
                      valueLabelDisplay="auto"
                      step={100}
                      marks
                      min={1000}
                      max={10000}
                    />
                  </>
                )}

                {/* 通用 LangChain 参数设置 */}
                {taskSettings.splitType && taskSettings.splitType !== 'markdown' && (
                  <>
                    <Typography id="chunk-size-slider" gutterBottom>
                      {t('settings.chunkSize')}: {taskSettings.chunkSize || 1500}
                    </Typography>
                    <Slider
                      value={taskSettings.chunkSize || 1500}
                      onChange={handleSliderChange('chunkSize')}
                      aria-labelledby="chunk-size-slider"
                      valueLabelDisplay="auto"
                      step={100}
                      marks
                      min={500}
                      max={8000}
                    />

                    <Typography id="chunk-overlap-slider" gutterBottom sx={{ mt: 3 }}>
                      {t('settings.chunkOverlap')}: {taskSettings.chunkOverlap || 200}
                    </Typography>
                    <Slider
                      value={taskSettings.chunkOverlap || 200}
                      onChange={handleSliderChange('chunkOverlap')}
                      aria-labelledby="chunk-overlap-slider"
                      valueLabelDisplay="auto"
                      step={50}
                      marks
                      min={0}
                      max={1000}
                    />
                  </>
                )}

                {/* Text 分块器特殊设置 */}
                {taskSettings.splitType === 'text' && (
                  <TextField
                    fullWidth
                    label={t('settings.separator')}
                    name="separator"
                    value={taskSettings.separator || '\\n\\n'}
                    onChange={handleSettingChange}
                    helperText={t('settings.separatorHelper')}
                    sx={{ mt: 3 }}
                  />
                )}

                {/* Code 分块器特殊设置 */}
                {taskSettings.splitType === 'code' && (
                  <FormControl fullWidth sx={{ mt: 3 }}>
                    <InputLabel id="code-language-label">{t('settings.codeLanguage')}</InputLabel>
                    <Select
                      labelId="code-language-label"
                      value={taskSettings.splitLanguage || 'js'}
                      label={t('settings.codeLanguage')}
                      name="splitLanguage"
                      onChange={handleSettingChange}
                    >
                      <MenuItem value="js">JavaScript</MenuItem>
                      <MenuItem value="python">Python</MenuItem>
                      <MenuItem value="java">Java</MenuItem>
                      <MenuItem value="go">Go</MenuItem>
                      <MenuItem value="ruby">Ruby</MenuItem>
                      <MenuItem value="cpp">C++</MenuItem>
                      <MenuItem value="c">C</MenuItem>
                      <MenuItem value="csharp">C#</MenuItem>
                      <MenuItem value="php">PHP</MenuItem>
                      <MenuItem value="rust">Rust</MenuItem>
                      <MenuItem value="typescript">TypeScript</MenuItem>
                      <MenuItem value="swift">Swift</MenuItem>
                      <MenuItem value="kotlin">Kotlin</MenuItem>
                      <MenuItem value="scala">Scala</MenuItem>
                    </Select>
                    <FormHelperText>{t('settings.codeLanguageHelper')}</FormHelperText>
                  </FormControl>
                )}

                {/* Recursive 分块器特殊设置 */}
                {taskSettings.splitType === 'recursive' && (
                  <Box sx={{ mt: 3 }}>
                    <Typography gutterBottom>{t('settings.separators')}</Typography>
                    <TextField
                      fullWidth
                      label={t('settings.separatorsInput')}
                      name="separatorsInput"
                      value={taskSettings.separatorsInput || '|,##,>,-'}
                      onChange={e => {
                        const value = e.target.value;
                        // 同时更新输入框值和分隔符数组
                        setTaskSettings(prev => ({
                          ...prev,
                          separatorsInput: value,
                          separators: value.split(',').map(item => item.trim())
                        }));
                      }}
                      helperText={t('settings.separatorsHelper')}
                    />
                    <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 1 }}>
                      {(taskSettings.separators || ['|', '##', '>', '-']).map((sep, index) => (
                        <Chip key={index} label={sep} variant="outlined" />
                      ))}
                    </Box>
                  </Box>
                )}

                <Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 3 }}>
                  {t('settings.textSplitDescription')}
                </Typography>
              </Box>
            </Grid>
          </Grid>
        </CardContent>
      </Card>
      <Card style={{ marginBottom: 20 }}>
        <CardContent>
          <Grid container spacing={3}>
            <Grid item xs={12}>
              <Typography variant="subtitle1" gutterBottom>
                {t('settings.questionGenSettings')}
              </Typography>
              <Box sx={{ px: 2, py: 1 }}>
                <Typography id="question-generation-length-slider" gutterBottom>
                  {t('settings.questionGenLength', { length: taskSettings.questionGenerationLength })}
                </Typography>
                <Slider
                  value={taskSettings.questionGenerationLength}
                  onChange={handleSliderChange('questionGenerationLength')}
                  aria-labelledby="question-generation-length-slider"
                  valueLabelDisplay="auto"
                  step={10}
                  marks
                  min={10}
                  max={1000}
                />
                <Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
                  {t('settings.questionGenDescription')}
                </Typography>

                <Typography id="question-mark-removing-probability-slider" gutterBottom sx={{ mt: 3 }}>
                  {t('settings.questionMaskRemovingProbability', {
                    probability: taskSettings.questionMaskRemovingProbability
                  })}
                </Typography>
                <Slider
                  value={taskSettings.questionMaskRemovingProbability}
                  onChange={handleSliderChange('questionMaskRemovingProbability')}
                  aria-labelledby="question-generation-length-slider"
                  valueLabelDisplay="auto"
                  step={5}
                  marks
                  min={0}
                  max={100}
                />

                <TextField
                  style={{ marginTop: 20 }}
                  fullWidth
                  label={t('settings.concurrencyLimit')}
                  name="concurrencyLimit"
                  value={taskSettings.concurrencyLimit}
                  onChange={handleSettingChange}
                  type="number"
                  helperText={t('settings.concurrencyLimitHelper')}
                />
              </Box>
            </Grid>
          </Grid>
        </CardContent>
      </Card>
      <Card style={{ marginBottom: 20 }}>
        <CardContent>
          <Grid container spacing={3}>
            <Grid item xs={12}>
              <Typography variant="subtitle1" gutterBottom>
                {t('settings.pdfSettings')}
              </Typography>
              <TextField
                fullWidth
                label={t('settings.minerUToken')}
                name="minerUToken"
                value={taskSettings.minerUToken}
                onChange={handleSettingChange}
                type="password"
                helperText={t('settings.minerUHelper')}
              />
            </Grid>
            <Grid item xs={12}>
              <TextField
                fullWidth
                label={t('settings.visionConcurrencyLimit')}
                name="visionConcurrencyLimit"
                value={taskSettings.visionConcurrencyLimit ? taskSettings.visionConcurrencyLimit : 5}
                onChange={handleSettingChange}
                type="number"
              />
            </Grid>
          </Grid>
        </CardContent>
      </Card>
      <Card>
        <CardContent>
          <Grid container spacing={3}>
            <Grid item xs={12}>
              <Typography variant="subtitle1" gutterBottom>
                {t('settings.datasetUpload')}
              </Typography>
              <TextField
                fullWidth
                label={t('settings.huggingfaceToken')}
                name="huggingfaceToken"
                value={taskSettings.huggingfaceToken}
                onChange={handleSettingChange}
                type="password"
                helperText={t('settings.huggingfaceNotImplemented')}
                InputProps={{
                  startAdornment: <InputAdornment position="start">hf_</InputAdornment>
                }}
              />
            </Grid>
          </Grid>
        </CardContent>
      </Card>
      <Snackbar
        open={success}
        autoHideDuration={2000}
        onClose={handleCloseSnackbar}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
      >
        <Alert onClose={handleCloseSnackbar} severity="success" sx={{ width: '100%' }}>
          {t('settings.saveSuccess')}
        </Alert>
      </Snackbar>
      <Snackbar
        open={!!error}
        autoHideDuration={2000}
        onClose={handleCloseSnackbar}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
      >
        <Alert onClose={handleCloseSnackbar} severity="error" sx={{ width: '100%' }}>
          {error}
        </Alert>
      </Snackbar>
      {/* 吸底保存按钮 */}
      <Box
        sx={{
          position: 'fixed',
          bottom: 0,
          left: 0,
          right: 0,
          padding: '8px',
          backgroundColor: 'background.paper',
          borderTop: '1px solid',
          borderColor: 'divider',
          zIndex: 1100,
          display: 'flex',
          justifyContent: 'center',
          boxShadow: 3
        }}
      >
        <Button
          variant="contained"
          color="primary"
          size="medium"
          startIcon={<SaveIcon />}
          onClick={handleSaveTaskSettings}
        >
          {t('settings.saveTaskConfig')}
        </Button>
      </Box>
    </Box>
  );
}

'use client';

import { useState } from 'react';
import {
  Box,
  Typography,
  ListItem,
  ListItemButton,
  ListItemIcon,
  ListItemText,
  IconButton,
  Collapse,
  Chip,
  Tooltip,
  List,
  CircularProgress
} from '@mui/material';
import FolderIcon from '@mui/icons-material/Folder';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import AddIcon from '@mui/icons-material/Add';
import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { useTranslation } from 'react-i18next';
import QuestionListItem from './QuestionListItem';

/**
 * 标签树项组件
 * @param {Object} props
 * @param {Object} props.tag - 标签对象
 * @param {number} props.level - 缩进级别
 * @param {boolean} props.expanded - 是否展开
 * @param {Function} props.onToggle - 切换展开/折叠的回调
 * @param {Function} props.onMenuOpen - 打开菜单的回调
 * @param {Function} props.onGenerateQuestions - 生成问题的回调
 * @param {Function} props.onGenerateSubTags - 生成子标签的回调
 * @param {Array} props.questions - 标签下的问题列表
 * @param {boolean} props.loadingQuestions - 是否正在加载问题
 * @param {Object} props.processingQuestions - 正在处理的问题ID映射
 * @param {Function} props.onDeleteQuestion - 删除问题的回调
 * @param {Function} props.onGenerateDataset - 生成数据集的回调
 * @param {Array} props.allQuestions - 所有问题列表（用于计算问题数量）
 * @param {Object} props.tagQuestions - 标签问题映射
 * @param {React.ReactNode} props.children - 子标签内容
 */
export default function TagTreeItem({
  tag,
  level = 0,
  expanded = false,
  onToggle,
  onMenuOpen,
  onGenerateQuestions,
  onGenerateSubTags,
  questions = [],
  loadingQuestions = false,
  processingQuestions = {},
  onDeleteQuestion,
  onGenerateDataset,
  allQuestions = [],
  tagQuestions = {},
  children
}) {
  const { t } = useTranslation();

  // 递归计算所有层级的子标签数量
  const getTotalSubTagsCount = childrenTags => {
    let count = childrenTags.length;
    childrenTags.forEach(childTag => {
      if (childTag.children && childTag.children.length > 0) {
        count += getTotalSubTagsCount(childTag.children);
      }
    });
    return count;
  };

  // 递归获取所有子标签的问题数量
  const getChildrenQuestionsCount = childrenTags => {
    let count = 0;
    childrenTags.forEach(childTag => {
      // 子标签的问题
      if (tagQuestions[childTag.id] && tagQuestions[childTag.id].length > 0) {
        count += tagQuestions[childTag.id].length;
      } else {
        count += allQuestions.filter(q => q.label === childTag.label).length;
      }

      // 子标签的子标签的问题
      if (childTag.children && childTag.children.length > 0) {
        count += getChildrenQuestionsCount(childTag.children);
      }
    });
    return count;
  };

  // 计算当前标签的问题数量
  const getCurrentTagQuestionsCount = () => {
    let currentTagQuestions = 0;
    if (tagQuestions[tag.id] && tagQuestions[tag.id].length > 0) {
      currentTagQuestions = tagQuestions[tag.id].length;
    } else {
      currentTagQuestions = allQuestions.filter(q => q.label === tag.label).length;
    }
    return currentTagQuestions;
  };

  // 总问题数量 = 当前标签的问题 + 所有子标签的问题
  const totalQuestions =
    getCurrentTagQuestionsCount() + (tag.children ? getChildrenQuestionsCount(tag.children || []) : 0);

  return (
    <Box key={tag.id} sx={{ my: 0.5 }}>
      <ListItem
        disablePadding
        sx={{
          pl: level * 2,
          borderLeft: level > 0 ? '1px dashed rgba(0, 0, 0, 0.1)' : 'none',
          ml: level > 0 ? 2 : 0
        }}
      >
        <ListItemButton onClick={() => onToggle(tag.id)} sx={{ borderRadius: 1, py: 0.5 }}>
          <ListItemIcon sx={{ minWidth: 36 }}>
            <FolderIcon color="primary" fontSize="small" />
          </ListItemIcon>
          <ListItemText
            primary={
              <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
                <Typography sx={{ fontWeight: 'medium' }}>{tag.label}</Typography>
                {tag.children && tag.children.length > 0 && (
                  <Chip
                    size="small"
                    label={`${getTotalSubTagsCount(tag.children)} ${t('distill.subTags')}`}
                    color="primary"
                    variant="outlined"
                    sx={{ height: 20, fontSize: '0.7rem' }}
                  />
                )}
                {totalQuestions > 0 && (
                  <Chip
                    size="small"
                    label={`${totalQuestions} ${t('distill.questions')}`}
                    color="secondary"
                    variant="outlined"
                    sx={{ height: 20, fontSize: '0.7rem' }}
                  />
                )}
              </Box>
            }
            primaryTypographyProps={{ component: 'div' }}
          />

          <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
            <Tooltip title={t('distill.generateQuestions')}>
              <IconButton
                size="small"
                onClick={e => {
                  e.stopPropagation();
                  onGenerateQuestions(tag);
                }}
              >
                <QuestionMarkIcon fontSize="small" />
              </IconButton>
            </Tooltip>

            <Tooltip title={t('distill.addChildTag')}>
              <IconButton
                size="small"
                onClick={e => {
                  e.stopPropagation();
                  onGenerateSubTags(tag);
                }}
              >
                <AddIcon fontSize="small" />
              </IconButton>
            </Tooltip>

            <IconButton size="small" onClick={e => onMenuOpen(e, tag)}>
              <MoreVertIcon fontSize="small" />
            </IconButton>

            {tag.children && tag.children.length > 0 ? (
              expanded ? (
                <ExpandLessIcon fontSize="small" />
              ) : (
                <ExpandMoreIcon fontSize="small" />
              )
            ) : null}
          </Box>
        </ListItemButton>
      </ListItem>

      {/* 子标签 */}
      {tag.children && tag.children.length > 0 && (
        <Collapse in={expanded} timeout="auto" unmountOnExit>
          {children}
        </Collapse>
      )}

      {/* 标签下的问题 */}
      {expanded && (
        <Collapse in={expanded} timeout="auto" unmountOnExit>
          <List disablePadding sx={{ mt: 0.5, mb: 1 }}>
            {loadingQuestions ? (
              <ListItem sx={{ pl: (level + 1) * 2, py: 0.75 }}>
                <CircularProgress size={20} />
                <Typography variant="body2" sx={{ ml: 2 }}>
                  {t('common.loading')}
                </Typography>
              </ListItem>
            ) : questions && questions.length > 0 ? (
              questions.map(question => (
                <QuestionListItem
                  key={question.id}
                  question={question}
                  level={level}
                  processing={processingQuestions[question.id]}
                  onDelete={e => onDeleteQuestion(question.id, e)}
                  onGenerateDataset={e => onGenerateDataset(question.id, question.question, e)}
                />
              ))
            ) : (
              <ListItem sx={{ pl: (level + 1) * 2, py: 1 }}>
                <Typography variant="body2" color="text.secondary">
                  {t('distill.noQuestions')}
                </Typography>
              </ListItem>
            )}
          </List>
        </Collapse>
      )}
    </Box>
  );
}

'use client';

import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
  Box,
  Paper,
  Typography,
  Divider,
  CircularProgress,
  Tabs,
  Tab,
  List,
  ListItem,
  ListItemText,
  Collapse,
  IconButton,
  TextField,
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogContentText,
  DialogTitle,
  Tooltip,
  Menu,
  MenuItem
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import TabPanel from './components/TabPanel';
import ReactMarkdown from 'react-markdown';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import axios from 'axios';
import { toast } from 'sonner';

/**
 * 领域分析组件
 * @param {Object} props
 * @param {string} props.projectId - 项目ID
 * @param {Array} props.toc - 目录结构数组
 * @param {Array} props.tags - 标签树数组
 * @param {boolean} props.loading - 是否加载中
 * @param {Function} props.onTagsUpdate - 标签更新回调
 */

// 领域树节点组件
function TreeNode({ node, level = 0, onEdit, onDelete, onAddChild }) {
  const [open, setOpen] = useState(true);
  const theme = useTheme();
  const hasChildren = node.child && node.child.length > 0;
  const [anchorEl, setAnchorEl] = useState(null);
  const menuOpen = Boolean(anchorEl);
  const { t } = useTranslation();

  const handleClick = () => {
    if (hasChildren) {
      setOpen(!open);
    }
  };

  const handleMenuOpen = event => {
    event.stopPropagation();
    setAnchorEl(event.currentTarget);
  };

  const handleMenuClose = event => {
    if (event) event.stopPropagation();
    setAnchorEl(null);
  };

  const handleEdit = event => {
    event.stopPropagation();
    onEdit(node);
    handleMenuClose();
  };

  const handleDelete = event => {
    event.stopPropagation();
    onDelete(node);
    handleMenuClose();
  };

  const handleAddChild = event => {
    event.stopPropagation();
    onAddChild(node);
    handleMenuClose();
  };

  return (
    <>
      <ListItem
        button
        onClick={handleClick}
        sx={{
          pl: level * 2 + 1,
          bgcolor: level === 0 ? theme.palette.primary.light : 'transparent',
          color: level === 0 ? theme.palette.primary.contrastText : 'inherit',
          '&:hover': {
            bgcolor: level === 0 ? theme.palette.primary.main : theme.palette.action.hover
          },
          borderRadius: '4px',
          mb: 0.5,
          pr: 1
        }}
      >
        <ListItemText
          primary={node.label}
          primaryTypographyProps={{
            fontWeight: level === 0 ? 600 : 400,
            fontSize: level === 0 ? '1rem' : '0.9rem'
          }}
        />
        <Box sx={{ display: 'flex', alignItems: 'center' }}>
          <IconButton
            size="small"
            onClick={handleMenuOpen}
            sx={{
              color: level === 0 ? 'inherit' : theme.palette.text.secondary,
              '&:hover': { bgcolor: 'rgba(255, 255, 255, 0.1)' }
            }}
          >
            <MoreVertIcon fontSize="small" />
          </IconButton>
          {hasChildren && (open ? <ExpandLess /> : <ExpandMore />)}
        </Box>

        <Menu anchorEl={anchorEl} open={menuOpen} onClose={handleMenuClose} onClick={e => e.stopPropagation()}>
          <MenuItem onClick={handleEdit}>
            <EditIcon fontSize="small" sx={{ mr: 1 }} />
            {t('textSplit.editTag')}
          </MenuItem>
          <MenuItem onClick={handleDelete}>
            <DeleteIcon fontSize="small" sx={{ mr: 1 }} />
            {t('textSplit.deleteTag')}
          </MenuItem>
          {level === 0 && (
            <MenuItem onClick={handleAddChild}>
              <AddIcon fontSize="small" sx={{ mr: 1 }} />
              {t('textSplit.addTag')}
            </MenuItem>
          )}
        </Menu>
      </ListItem>

      {hasChildren && (
        <Collapse in={open} timeout="auto" unmountOnExit>
          <List component="div" disablePadding>
            {node.child.map((childNode, index) => (
              <TreeNode
                key={index}
                node={childNode}
                level={level + 1}
                onEdit={onEdit}
                onDelete={onDelete}
                onAddChild={onAddChild}
              />
            ))}
          </List>
        </Collapse>
      )}
    </>
  );
}

// 领域树组件
function DomainTree({ tags, onEdit, onDelete, onAddChild }) {
  return (
    <List component="nav" aria-label="domain tree">
      {tags.map((node, index) => (
        <TreeNode key={index} node={node} onEdit={onEdit} onDelete={onDelete} onAddChild={onAddChild} />
      ))}
    </List>
  );
}

export default function DomainAnalysis({ projectId, toc = '', loading = false }) {
  const theme = useTheme();
  const { t } = useTranslation();
  const [activeTab, setActiveTab] = useState(0);
  const [dialogOpen, setDialogOpen] = useState(false);
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  const [currentNode, setCurrentNode] = useState(null);
  const [parentNode, setParentNode] = useState('');
  const [dialogMode, setDialogMode] = useState('add');
  const [labelValue, setLabelValue] = useState({});
  const [saving, setSaving] = useState(false);
  const [tags, setTags] = useState([]);
  const [snackbar, setSnackbar] = useState({
    open: false,
    message: '',
    severity: 'success'
  });

  const handleCloseSnackbar = () => {
    setSnackbar(prev => ({ ...prev, open: false }));
  };

  useEffect(() => {
    getTags();
  }, []);
  const getTags = async () => {
    const response = await axios.get(`/api/projects/${projectId}/tags`);
    setTags(response.data.tags);
  };
  // 处理标签切换
  const handleTabChange = (event, newValue) => {
    setActiveTab(newValue);
  };

  // 打开添加标签对话框
  const handleAddTag = () => {
    setDialogMode('add');
    setCurrentNode(null);
    setParentNode(null);
    setLabelValue({});
    setDialogOpen(true);
  };

  // 打开编辑标签对话框
  const handleEditTag = node => {
    setDialogMode('edit');
    setCurrentNode({ id: node.id, label: node.label });
    setLabelValue({ id: node.id, label: node.label });
    setDialogOpen(true);
  };

  // 打开添加子标签对话框
  const handleAddChildTag = parentNode => {
    setDialogMode('addChild');
    setParentNode(parentNode.label);
    setLabelValue({ parentId: parentNode.id });
    setDialogOpen(true);
  };

  // 打开删除标签对话框
  const handleDeleteTag = node => {
    setCurrentNode(node);
    setDeleteDialogOpen(true);
  };

  // 关闭对话框
  const handleCloseDialog = () => {
    setDialogOpen(false);
    setDeleteDialogOpen(false);
  };

  // 查找并更新节点
  const findAndUpdateNode = (nodes, targetNode, newLabel) => {
    return nodes.map(node => {
      if (node === targetNode) {
        return { ...node, label: newLabel };
      }
      if (node.child && node.child.length > 0) {
        return { ...node, child: findAndUpdateNode(node.child, targetNode, newLabel) };
      }
      return node;
    });
  };

  // 查找并删除节点
  const findAndDeleteNode = (nodes, targetNode) => {
    return nodes
      .filter(node => node !== targetNode)
      .map(node => {
        if (node.child && node.child.length > 0) {
          return { ...node, child: findAndDeleteNode(node.child, targetNode) };
        }
        return node;
      });
  };

  // 查找并添加子节点
  const findAndAddChildNode = (nodes, parentNode, childLabel) => {
    return nodes.map(node => {
      if (node === parentNode) {
        const childArray = node.child || [];
        return {
          ...node,
          child: [...childArray, { label: childLabel, child: [] }]
        };
      }
      if (node.child && node.child.length > 0) {
        return { ...node, child: findAndAddChildNode(node.child, parentNode, childLabel) };
      }
      return node;
    });
  };

  // 保存标签更改
  const saveTagChanges = async updatedTags => {
    console.log('保存标签更改:', updatedTags);
    setSaving(true);
    try {
      const response = await fetch(`/api/projects/${projectId}/tags`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ tags: updatedTags })
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.error || t('domain.errors.saveFailed'));
      }
      getTags();
      setSnackbar({
        open: true,
        message: t('domain.messages.updateSuccess'),
        severity: 'success'
      });
    } catch (error) {
      console.error('保存标签失败:', error);
      setSnackbar({
        open: true,
        message: error.message || '保存标签失败',
        severity: 'error'
      });
    } finally {
      setSaving(false);
    }
  };

  // 提交表单
  const handleSubmit = async () => {
    if (!labelValue.label.trim()) {
      setSnackbar({
        open: true,
        message: '标签名称不能为空',
        severity: 'error'
      });
      return;
    }

    await saveTagChanges(labelValue);
    handleCloseDialog();
  };

  const handleConfirmDelete = async () => {
    if (!currentNode) return;

    const res = await axios.delete(`/api/projects/${projectId}/tags?id=${currentNode.id}`);
    if (res.status === 200) {
      toast.success('删除成功');
      getTags();
    }

    setDeleteDialogOpen(false);
  };

  if (loading) {
    return (
      <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
        <CircularProgress />
      </Box>
    );
  }

  if (toc.length === 0) {
    return (
      <Paper
        sx={{
          p: 4,
          textAlign: 'center',
          border: `1px solid ${theme.palette.divider}`,
          borderRadius: 2
        }}
      >
        <Typography variant="body1" color="textSecondary">
          {t('domain.noToc')}
        </Typography>
      </Paper>
    );
  }

  return (
    <Box>
      <Paper
        sx={{
          p: 0,
          mb: 3,
          border: `1px solid ${theme.palette.divider}`,
          borderRadius: 2,
          overflow: 'hidden'
        }}
      >
        <Tabs
          value={activeTab}
          onChange={handleTabChange}
          variant="fullWidth"
          indicatorColor="secondary"
          sx={{
            borderBottom: 1,
            borderColor: 'divider',
            bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.03)',
            borderTopLeftRadius: 2,
            borderTopRightRadius: 2
          }}
        >
          <Tab label={t('domain.tabs.tree')} />
          <Tab label={t('domain.tabs.structure')} />
        </Tabs>

        <Box
          sx={{
            p: 3,
            bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.02)' : 'rgba(255, 255, 255, 0.8)',
            borderBottomLeftRadius: 2,
            borderBottomRightRadius: 2,
            boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.03)'
          }}
        >
          <TabPanel value={activeTab} index={0}>
            <Box>
              <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
                <Typography variant="h6">{t('domain.tabs.tree')}</Typography>
                <Tooltip title="添加一级标签">
                  <Button variant="outlined" size="small" startIcon={<AddIcon />} onClick={handleAddTag}>
                    {t('domain.addRootTag')}
                  </Button>
                </Tooltip>
              </Box>
              <Divider sx={{ mb: 2 }} />
              <Box
                sx={{
                  p: 2,
                  bgcolor: theme.palette.background.paper,
                  borderRadius: 1,
                  maxHeight: '800px',
                  overflow: 'auto'
                }}
              >
                {tags && tags.length > 0 ? (
                  <DomainTree
                    tags={tags}
                    onEdit={handleEditTag}
                    onDelete={handleDeleteTag}
                    onAddChild={handleAddChildTag}
                  />
                ) : (
                  <Box sx={{ textAlign: 'center', py: 4 }}>
                    <Typography variant="body2" color="textSecondary" gutterBottom>
                      {t('domain.noTags')}
                    </Typography>
                    <Button
                      variant="outlined"
                      size="small"
                      startIcon={<AddIcon />}
                      onClick={handleAddTag}
                      sx={{ mt: 1 }}
                    >
                      {t('domain.addFirstTag')}
                    </Button>
                  </Box>
                )}
              </Box>
            </Box>
          </TabPanel>
          <TabPanel value={activeTab} index={1}>
            <Box>
              <Typography variant="h6" gutterBottom>
                {t('domain.docStructure')}
              </Typography>
              <Divider sx={{ mb: 2 }} />
              <Box
                sx={{
                  p: 2,
                  bgcolor: theme.palette.background.paper,
                  borderRadius: 1,
                  maxHeight: '600px',
                  overflow: 'auto'
                }}
              >
                <ReactMarkdown
                  components={{
                    root: ({ children }) => (
                      <div
                        style={{
                          fontFamily: 'monospace',
                          whiteSpace: 'pre-wrap',
                          wordBreak: 'break-word'
                        }}
                      >
                        {children}
                      </div>
                    )
                  }}
                >
                  {toc}
                </ReactMarkdown>
              </Box>
            </Box>
          </TabPanel>
        </Box>
      </Paper>

      {/* 添加/编辑标签对话框 */}
      <Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
        <DialogTitle>
          {dialogMode === 'add'
            ? t('domain.dialog.addTitle')
            : dialogMode === 'edit'
              ? t('domain.dialog.editTitle')
              : t('domain.dialog.addChildTitle')}
        </DialogTitle>
        <DialogContent>
          <DialogContentText sx={{ mb: 2 }}>
            {dialogMode === 'add'
              ? t('domain.dialog.inputRoot')
              : dialogMode === 'edit'
                ? t('domain.dialog.inputEdit')
                : t('domain.dialog.inputChild', { label: parentNode })}
          </DialogContentText>
          <TextField
            autoFocus
            margin="dense"
            label={t('domain.dialog.labelName')}
            type="text"
            fullWidth
            variant="outlined"
            value={labelValue.label}
            onChange={e => setLabelValue({ ...labelValue, label: e.target.value })}
          />
        </DialogContent>
        <DialogActions>
          <Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
          <Button onClick={handleSubmit} variant="contained" disabled={saving || !labelValue?.label?.trim()}>
            {saving ? t('common.saving') : t('common.save')}
          </Button>
        </DialogActions>
      </Dialog>

      {/* 删除确认对话框 */}
      <Dialog open={deleteDialogOpen} onClose={handleCloseDialog}>
        <DialogTitle>{t('common.confirmDelete')}</DialogTitle>
        <DialogContent>
          <DialogContentText>
            {t('domain.dialog.deleteConfirm', { label: currentNode?.label })}
            {currentNode?.child && currentNode.child.length > 0 && t('domain.dialog.deleteWarning')}
          </DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
          <Button onClick={handleConfirmDelete} color="error" variant="contained">
            {saving ? t('common.deleting') : t('common.delete')}
          </Button>
        </DialogActions>
      </Dialog>
    </Box>
  );
}

import { NextResponse } from 'next/server';
import { getQuestionsForChunk } from '@/lib/db/questions';
import logger from '@/lib/util/logger';
import questionService from '@/lib/services/questions';

// 为指定文本块生成问题
export async function POST(request, { params }) {
  try {
    const { projectId, chunkId } = params;

    // 验证项目ID和文本块ID
    if (!projectId || !chunkId) {
      return NextResponse.json({ error: 'Project ID or text block ID cannot be empty' }, { status: 400 });
    } // 获取请求体
    const { model, language = '中文', number, enableGaExpansion = false } = await request.json();

    if (!model) {
      return NextResponse.json({ error: 'Model cannot be empty' }, { status: 400 });
    }

    // 后续会根据是否有GA对来选择是否启用GA扩展选择服务函数
    const serviceFunc = questionService.generateQuestionsForChunkWithGA;

    // 使用问题生成服务
    const result = await serviceFunc(projectId, chunkId, {
      model,
      language,
      number,
      enableGaExpansion
    });

    // 统一返回格式，确保包含GA扩展信息
    const response = {
      chunkId,
      questions: result.questions || result.labelQuestions || [],
      total: result.total || (result.questions || result.labelQuestions || []).length,
      gaExpansionUsed: result.gaExpansionUsed || false,
      gaPairsCount: result.gaPairsCount || 0,
      expectedTotal: result.expectedTotal || result.total
    };

    // 返回生成的问题
    return NextResponse.json(response);
  } catch (error) {
    logger.error('Error generating questions:', error);
    return NextResponse.json({ error: error.message || 'Error generating questions' }, { status: 500 });
  }
}

// 获取指定文本块的问题
export async function GET(request, { params }) {
  try {
    const { projectId, chunkId } = params;

    // 验证项目ID和文本块ID
    if (!projectId || !chunkId) {
      return NextResponse.json({ error: 'The item ID or text block ID cannot be empty' }, { status: 400 });
    }

    // 获取文本块的问题
    const questions = await getQuestionsForChunk(projectId, chunkId);

    // 返回问题列表
    return NextResponse.json({
      chunkId,
      questions,
      total: questions.length
    });
  } catch (error) {
    console.error('Error getting questions:', String(error));
    return NextResponse.json({ error: error.message || 'Error getting questions' }, { status: 500 });
  }
}

import { NextResponse } from 'next/server';
import { getDatasetsById, updateDataset } from '@/lib/db/datasets';
import LLMClient from '@/lib/llm/core/index';
import getNewAnswerPrompt from '@/lib/llm/prompts/newAnswer';
import getNewAnswerEnPrompt from '@/lib/llm/prompts/newAnswerEn';
import { extractJsonFromLLMOutput } from '@/lib/llm/common/util';

// 优化数据集答案
export async function POST(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: 'Project ID cannot be empty' }, { status: 400 });
    }

    // 获取请求体
    const { datasetId, model, advice, language } = await request.json();

    if (!datasetId) {
      return NextResponse.json({ error: 'Dataset ID cannot be empty' }, { status: 400 });
    }

    if (!model) {
      return NextResponse.json({ error: 'Model cannot be empty' }, { status: 400 });
    }

    if (!advice) {
      return NextResponse.json({ error: 'Please provide optimization suggestions' }, { status: 400 });
    }

    // 获取数据集内容
    const dataset = await getDatasetsById(datasetId);
    if (!dataset) {
      return NextResponse.json({ error: 'Dataset does not exist' }, { status: 404 });
    }

    // 创建LLM客户端
    const llmClient = new LLMClient(model);

    // 生成优化后的答案和思维链
    const prompt =
      language === 'en'
        ? getNewAnswerEnPrompt(dataset.question, dataset.answer || '', dataset.cot || '', advice)
        : getNewAnswerPrompt(dataset.question, dataset.answer || '', dataset.cot || '', advice);

    const response = await llmClient.getResponse(prompt);

    // 从LLM输出中提取JSON格式的优化结果
    const optimizedResult = extractJsonFromLLMOutput(response);

    if (!optimizedResult || !optimizedResult.answer) {
      return NextResponse.json({ error: 'Failed to optimize answer, please try again' }, { status: 500 });
    }

    // 更新数据集
    const updatedDataset = {
      ...dataset,
      answer: optimizedResult.answer,
      cot: optimizedResult.cot || dataset.cot
    };

    await updateDataset(updatedDataset);

    // 返回优化后的数据集
    return NextResponse.json({
      success: true,
      dataset: updatedDataset
    });
  } catch (error) {
    console.error('Failed to optimize answer:', String(error));
    return NextResponse.json({ error: error.message || 'Failed to optimize answer' }, { status: 500 });
  }
}

'use client';

import React from 'react';
import { Box, Typography, Paper, Alert } from '@mui/material';
import { useParams } from 'next/navigation';
import { useTheme } from '@mui/material/styles';
import ChatArea from '@/components/playground/ChatArea';
import MessageInput from '@/components/playground/MessageInput';
import PlaygroundHeader from '@/components/playground/PlaygroundHeader';
import useModelPlayground from '@/hooks/useModelPlayground';
import { playgroundStyles } from '@/styles/playground';
import { useTranslation } from 'react-i18next';
import { useAtomValue } from 'jotai/index';
import { modelConfigListAtom } from '@/lib/store';

export default function ModelPlayground({ searchParams }) {
  const theme = useTheme();
  const params = useParams();
  const { projectId } = params;
  const modelId = searchParams?.modelId || null;
  const styles = playgroundStyles(theme);
  const { t } = useTranslation();

  const {
    selectedModels,
    loading,
    userInput,
    conversations,
    error,
    outputMode,
    uploadedImage,
    handleModelSelection,
    handleInputChange,
    handleImageUpload,
    handleRemoveImage,
    handleSendMessage,
    handleClearConversations,
    handleOutputModeChange
  } = useModelPlayground(projectId, modelId);

  const availableModels = useAtomValue(modelConfigListAtom);

  // 获取模型名称
  const getModelName = modelId => {
    const model = availableModels.find(m => m.id === modelId);
    return model ? `${model.providerName}: ${model.modelName}` : modelId;
  };
  return (
    <Box sx={styles.container}>
      <Typography variant="h5" component="h1" gutterBottom>
        {t('playground.title')}
      </Typography>

      {error && (
        <Alert severity="error" sx={{ mb: 2 }}>
          {error}
        </Alert>
      )}

      <Paper elevation={2} sx={styles.mainPaper}>
        <PlaygroundHeader
          availableModels={availableModels}
          selectedModels={selectedModels}
          handleModelSelection={handleModelSelection}
          handleClearConversations={handleClearConversations}
          conversations={conversations}
          outputMode={outputMode}
          handleOutputModeChange={handleOutputModeChange}
        />

        <ChatArea
          selectedModels={selectedModels}
          conversations={conversations}
          loading={loading}
          getModelName={getModelName}
        />

        <MessageInput
          userInput={userInput}
          handleInputChange={handleInputChange}
          handleSendMessage={handleSendMessage}
          loading={loading}
          selectedModels={selectedModels}
          uploadedImage={uploadedImage}
          handleImageUpload={handleImageUpload}
          handleRemoveImage={handleRemoveImage}
          availableModels={availableModels}
        />
      </Paper>
    </Box>
  );
}

'use client';

import { useState, useEffect } from 'react';
import { Typography, Box, Button, TextField, Grid, Card, CardContent, Alert, Snackbar } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import { useTranslation } from 'react-i18next';

export default function BasicSettings({ projectId }) {
  const { t } = useTranslation();
  const [projectInfo, setProjectInfo] = useState({
    id: '',
    name: '',
    description: ''
  });
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);

  useEffect(() => {
    async function fetchProjectInfo() {
      try {
        setLoading(true);
        const response = await fetch(`/api/projects/${projectId}`);

        if (!response.ok) {
          throw new Error(t('projects.fetchFailed'));
        }

        const data = await response.json();
        setProjectInfo(data);
      } catch (error) {
        console.error('获取项目信息出错:', error);
        setError(error.message);
      } finally {
        setLoading(false);
      }
    }

    fetchProjectInfo();
  }, [projectId, t]);

  // 处理项目信息变更
  const handleProjectInfoChange = e => {
    const { name, value } = e.target;
    setProjectInfo(prev => ({
      ...prev,
      [name]: value
    }));
  };

  // 保存项目信息
  const handleSaveProjectInfo = async () => {
    try {
      const response = await fetch(`/api/projects/${projectId}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          name: projectInfo.name,
          description: projectInfo.description
        })
      });

      if (!response.ok) {
        throw new Error(t('projects.saveFailed'));
      }

      setSuccess(true);
    } catch (error) {
      console.error('保存项目信息出错:', error);
      setError(error.message);
    }
  };

  const handleCloseSnackbar = () => {
    setSuccess(false);
    setError(null);
  };

  if (loading) {
    return <Typography>{t('common.loading')}</Typography>;
  }

  return (
    <Card>
      <CardContent>
        <Typography variant="h6" gutterBottom>
          {t('settings.basicInfo')}
        </Typography>
        <Grid container spacing={3}>
          <Grid item xs={12}>
            <TextField
              fullWidth
              label={t('projects.id')}
              value={projectInfo.id}
              disabled
              helperText={t('settings.idNotEditable')}
            />
          </Grid>
          <Grid item xs={12}>
            <TextField
              fullWidth
              label={t('projects.name')}
              name="name"
              value={projectInfo.name}
              onChange={handleProjectInfoChange}
              required
            />
          </Grid>
          <Grid item xs={12}>
            <TextField
              fullWidth
              label={t('projects.description')}
              name="description"
              value={projectInfo.description}
              onChange={handleProjectInfoChange}
              multiline
              rows={3}
            />
          </Grid>
          <Grid item xs={12}>
            <Button variant="contained" startIcon={<SaveIcon />} onClick={handleSaveProjectInfo}>
              {t('settings.saveBasicInfo')}
            </Button>
          </Grid>
        </Grid>
      </CardContent>

      <Snackbar
        open={success}
        autoHideDuration={2000}
        onClose={handleCloseSnackbar}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
      >
        <Alert onClose={handleCloseSnackbar} severity="success" sx={{ width: '100%' }}>
          {t('settings.saveSuccess')}
        </Alert>
      </Snackbar>

      <Snackbar
        open={!!error}
        autoHideDuration={2000}
        onClose={handleCloseSnackbar}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
      >
        <Alert onClose={handleCloseSnackbar} severity="error" sx={{ width: '100%' }}>
          {error}
        </Alert>
      </Snackbar>
    </Card>
  );
}

'use client';

import React, { useEffect, useState, useMemo } from 'react';
import { FormControl, Select, MenuItem, useTheme, ListSubheader, Box } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { useAtom, useAtomValue } from 'jotai/index';
import { modelConfigListAtom, selectedModelInfoAtom } from '@/lib/store';
import axios from 'axios';

// 获取模型对应的图标路径
const getModelIcon = modelName => {
  if (!modelName) return '/imgs/models/default.svg';

  // 将模型名称转换为小写以便比较
  const lowerModelName = modelName.toLowerCase();

  // 定义已知模型前缀映射
  const modelPrefixes = [
    { prefix: 'doubao', icon: 'doubao.svg' },
    { prefix: 'qwen', icon: 'qwen.svg' },
    { prefix: 'gpt', icon: 'gpt.svg' },
    { prefix: 'gemini', icon: 'gemini.svg' },
    { prefix: 'claude', icon: 'claude.svg' },
    { prefix: 'llama', icon: 'llama.svg' },
    { prefix: 'mistral', icon: 'mistral.svg' },
    { prefix: 'yi', icon: 'yi.svg' },
    { prefix: 'deepseek', icon: 'deepseek.svg' },
    { prefix: 'chatglm', icon: 'chatglm.svg' },
    { prefix: 'wenxin', icon: 'wenxin.svg' },
    { prefix: 'glm', icon: 'glm.svg' },
    { prefix: 'hunyuan', icon: 'hunyuan.svg' }

    // 添加更多模型前缀映射...
  ];

  // 查找匹配的模型前缀
  const matchedPrefix = modelPrefixes.find(({ prefix }) => lowerModelName.includes(prefix));

  // 返回对应的图标路径，如果没有匹配则返回默认图标
  return `/imgs/models/${matchedPrefix ? matchedPrefix.icon : 'default.svg'}`;
};

export default function ModelSelect({
  size = 'small',
  minWidth = 50,
  projectId,
  minHeight = 36,
  required = false,
  onError
}) {
  const theme = useTheme();
  const { t } = useTranslation();
  const models = useAtomValue(modelConfigListAtom);
  const [selectedModelInfo, setSelectedModelInfo] = useAtom(selectedModelInfoAtom);
  // 确保始终使用字符串值初始化 selectedModel，避免从非受控变为受控
  const [selectedModel, setSelectedModel] = useState(() => {
    if (selectedModelInfo && selectedModelInfo.id) {
      return selectedModelInfo.id;
    } else if (models && models.length > 0 && models[0]?.id) {
      return models[0].id;
    }
    return '';
  });
  const [error, setError] = useState(false);
  const handleModelChange = event => {
    if (!event || !event.target) return;
    const newModelId = event.target.value;

    // 清除错误状态
    if (error) {
      setError(false);
      if (onError) onError(false);
    }

    // 找到选中的模型对象
    const selectedModelObj = models.find(model => model.id === newModelId);
    if (selectedModelObj) {
      setSelectedModel(newModelId);
      // 将完整的模型信息存储到 localStorage
      setSelectedModelInfo(selectedModelObj);
      updateDefaultModel(newModelId);
    } else {
      setSelectedModelInfo({
        id: newModelId
      });
    }
  };

  const updateDefaultModel = async id => {
    const res = await axios.put(`/api/projects/${projectId}`, { projectId, defaultModelConfigId: id });
    if (res.status === 200) {
      console.log('更新成功');
    }
  };

  // 检查是否选择了模型
  const validateModel = () => {
    if (required && (!selectedModel || selectedModel === '')) {
      setError(true);
      if (onError) onError(true);
      return false;
    }
    return true;
  };

  useEffect(() => {
    if (selectedModelInfo && selectedModelInfo.id) {
      setSelectedModel(selectedModelInfo.id);
    }
  }, [selectedModelInfo]);

  // 初始检查
  useEffect(() => {
    if (required) {
      validateModel();
    }
  }, [required]);

  // 获取当前选中模型的显示内容
  const renderSelectedValue = value => {
    const selectedModelObj = models.find(model => model.id === value);
    if (!selectedModelObj) return null;

    return (
      <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
        <Box
          component="img"
          src={getModelIcon(selectedModelObj.modelName)}
          alt={selectedModelObj.modelName}
          sx={{
            width: 20,
            height: 20,
            objectFit: 'contain',
            flexShrink: 0,
            background: '#ffffffc9',
            borderRadius: '50%',
            marginBottom: '-2px'
          }}
          onError={e => {
            e.target.src = '/imgs/models/default.svg';
          }}
        />
        {selectedModelObj.modelName}
      </Box>
    );
  };

  return (
    <FormControl size={size} sx={{ minWidth, minHeight }} error={error}>
      <Select
        value={selectedModel}
        onChange={handleModelChange}
        displayEmpty
        variant="outlined"
        onBlur={validateModel}
        renderValue={renderSelectedValue}
        sx={{
          bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(255, 255, 255, 0.15)',
          color: theme.palette.mode === 'dark' ? 'inherit' : 'white',
          borderRadius: '8px',
          '& .MuiSelect-select': {
            display: 'flex',
            alignItems: 'center',
            padding: '6px 32px 6px 12px'
          },
          '& .MuiSelect-icon': {
            color: theme.palette.mode === 'dark' ? 'inherit' : 'white',
            right: '8px'
          },
          '& .MuiOutlinedInput-notchedOutline': {
            borderColor: 'transparent'
          },
          '&:hover .MuiOutlinedInput-notchedOutline': {
            borderColor: 'transparent'
          },
          '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
            borderColor: 'primary.main'
          },
          marginRight: '-14px',
          minHeight: '36px'
        }}
        MenuProps={{
          PaperProps: {
            elevation: 2,
            sx: {
              mt: 1,
              borderRadius: 2,
              '& .MuiMenuItem-root': {
                minHeight: '30px'
              }
            }
          }
        }}
      >
        <MenuItem value="" disabled>
          {error ? t('models.pleaseSelectModel') : t('playground.selectModelFirst')}
        </MenuItem>
        {(() => {
          // 按 provider 分组
          const filteredModels = models.filter(m => {
            if (m.providerId?.toLowerCase() === 'ollama') {
              return m.modelName && m.endpoint;
            } else {
              return m.modelName && m.endpoint && m.apiKey;
            }
          });

          // 获取所有 provider
          const providers = [...new Set(filteredModels.map(m => m.providerName || 'Other'))];

          return providers.map(provider => {
            const providerModels = filteredModels.filter(m => (m.providerName || 'Other') === provider);
            return [
              <ListSubheader
                key={`header-${provider}`}
                sx={{
                  pl: 2,
                  color: theme.palette.text.secondary,
                  fontWeight: 500,
                  mt: 1,
                  mb: 0.5
                }}
              >
                {provider || 'Other'}
              </ListSubheader>,
              ...providerModels.map(model => (
                <MenuItem
                  key={model.id}
                  value={model.id}
                  sx={{
                    pl: 3,
                    display: 'flex',
                    alignItems: 'center',
                    gap: 2,
                    minHeight: '30px',
                    '&.Mui-selected': {
                      bgcolor: theme.palette.action.selected,
                      '&:hover': {
                        bgcolor: theme.palette.action.selected
                      }
                    }
                  }}
                >
                  <Box
                    component="img"
                    src={getModelIcon(model.modelName)}
                    alt={model.modelName}
                    sx={{
                      width: 20,
                      height: 20,
                      objectFit: 'contain',
                      flexShrink: 0
                    }}
                    onError={e => {
                      e.target.src = '/imgs/models/default.svg';
                    }}
                  />
                  <Box component="span" sx={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }}>
                    {model.modelName}
                  </Box>
                </MenuItem>
              ))
            ];
          });
        })()}
      </Select>
    </FormControl>
  );
}

'use client';

import { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  TextField,
  Button,
  Box,
  Autocomplete,
  TextField as MuiTextField
} from '@mui/material';
import axios from 'axios';

export default function QuestionEditDialog({
  open,
  onClose,
  onSubmit,
  initialData,
  projectId,
  tags,
  mode = 'create' // 'create' or 'edit'
}) {
  const [chunks, setChunks] = useState([]);
  const { t } = useTranslation();
  // 获取文本块的标题
  const getChunkTitle = chunkId => {
    const chunk = chunks.find(c => c.id === chunkId);
    return chunk?.name || chunkId; // 直接使用文件名
  };

  const [formData, setFormData] = useState({
    id: '',
    question: '',
    chunkId: '',
    label: '' // 默认不选中任何标签
  });

  const getChunks = async projectId => {
    // 获取文本块列表
    const response = await axios.get(`/api/projects/${projectId}/split`);
    if (response.status !== 200) {
      throw new Error(t('common.fetchError'));
    }
    setChunks(response.data.chunks || []);
  };

  useEffect(() => {
    getChunks(projectId);
    if (initialData) {
      console.log('初始数据:', initialData); // 查看传入的初始数据
      setFormData({
        id: initialData.id,
        question: initialData.question || '',
        chunkId: initialData.chunkId || '',
        label: initialData.label || 'other' // 改用 label 而不是 label
      });
    } else {
      setFormData({
        id: '',
        question: '',
        chunkId: '',
        label: ''
      });
    }
  }, [initialData]);

  const handleSubmit = () => {
    onSubmit(formData);
    onClose();
  };

  const flattenTags = (tags, prefix = '') => {
    let flatTags = [];
    const traverse = node => {
      flatTags.push({
        id: node.label, // 使用标签名作为 id
        label: node.label, // 直接使用原始标签名
        originalLabel: node.label
      });
      if (node.child && node.child.length > 0) {
        node.child.forEach(child => traverse(child));
      }
    };
    tags.forEach(tag => traverse(tag));
    flatTags.push({
      id: 'other',
      label: t('datasets.uncategorized'),
      originalLabel: 'other'
    });
    return flatTags;
  };

  const flattenedTags = useMemo(() => flattenTags(tags), [tags, t]);

  // 修改 return 中的 Autocomplete 组件
  <Autocomplete
    fullWidth
    options={flattenedTags}
    getOptionLabel={tag => {
      return tag.label;
    }}
    value={(() => {
      const foundTag = flattenedTags.find(tag => tag.id === formData.label);
      const defaultTag = flattenedTags.find(tag => tag.id === 'other');
      return foundTag || defaultTag;
    })()}
    onChange={(e, newValue) => {
      setFormData({ ...formData, label: newValue ? newValue.id : 'other' });
    }}
    renderInput={params => (
      <MuiTextField {...params} label={t('questions.selectTag')} placeholder={t('questions.searchTag')} />
    )}
  />;

  return (
    <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
      <DialogTitle>{mode === 'create' ? t('questions.createQuestion') : t('questions.editQuestion')}</DialogTitle>
      <DialogContent>
        <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }}>
          <TextField
            label={t('questions.questionContent')}
            multiline
            rows={4}
            fullWidth
            value={formData.question}
            onChange={e => setFormData({ ...formData, question: e.target.value })}
          />

          <Autocomplete
            fullWidth
            options={chunks}
            getOptionLabel={chunk => getChunkTitle(chunk.id)}
            value={chunks.find(chunk => chunk.id === formData.chunkId) || null}
            onChange={(e, newValue) => setFormData({ ...formData, chunkId: newValue ? newValue.id : '' })}
            renderInput={params => (
              <MuiTextField {...params} label={t('questions.selectChunk')} placeholder={t('questions.searchChunk')} />
            )}
          />

          <Autocomplete
            fullWidth
            options={flattenedTags}
            getOptionLabel={tag => tag.label}
            value={flattenedTags.find(tag => tag.id === formData.label) || null}
            onChange={(e, newValue) => setFormData({ ...formData, label: newValue ? newValue.id : '' })}
            renderInput={params => (
              <MuiTextField {...params} label={t('questions.selectTag')} placeholder={t('questions.searchTag')} />
            )}
          />
        </Box>
      </DialogContent>
      <DialogActions>
        <Button onClick={onClose}>{t('common.cancel')}</Button>
        <Button onClick={handleSubmit} variant="contained" disabled={!formData.question || !formData.chunkId}>
          {mode === 'create' ? t('common.create') : t('common.save')}
        </Button>
      </DialogActions>
    </Dialog>
  );
}

import { createProject, getProjects, isExistByName } from '@/lib/db/projects';
import { createInitModelConfig, getModelConfigByProjectId } from '@/lib/db/model-config';

export async function POST(request) {
  try {
    const projectData = await request.json();
    // 验证必要的字段
    if (!projectData.name) {
      return Response.json({ error: '项目名称不能为空' }, { status: 400 });
    }

    // 验证项目名称是否已存在
    if (await isExistByName(projectData.name)) {
      return Response.json({ error: '项目名称已存在' }, { status: 400 });
    }
    // 创建项目
    const newProject = await createProject(projectData);
    // 如果指定了要复用的项目配置
    if (projectData.reuseConfigFrom) {
      let data = await getModelConfigByProjectId(projectData.reuseConfigFrom);

      let newData = data.map(item => {
        delete item.id;
        return {
          ...item,
          projectId: newProject.id
        };
      });
      await createInitModelConfig(newData);
    }
    return Response.json(newProject, { status: 201 });
  } catch (error) {
    console.error('创建项目出错:', String(error));
    return Response.json({ error: String(error) }, { status: 500 });
  }
}

export async function GET(request) {
  try {
    // 获取所有项目
    const projects = await getProjects();
    return Response.json(projects);
  } catch (error) {
    console.error('获取项目列表出错:', String(error));
    return Response.json({ error: String(error) }, { status: 500 });
  }
}

import { NextResponse } from 'next/server';
import LLMClient from '@/lib/llm/core/index';

export async function POST(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
    }

    // 获取请求体
    const { model, messages } = await request.json();

    // 验证请求参数
    if (!model) {
      return NextResponse.json({ error: 'The model parameters cannot be empty' }, { status: 400 });
    }

    if (!Array.isArray(messages) || messages.length === 0) {
      return NextResponse.json({ error: 'The message list cannot be empty' }, { status: 400 });
    }

    // 使用自定义的LLM客户端
    const llmClient = new LLMClient(model);

    // 格式化消息历史
    const formattedMessages = messages.map(msg => {
      // 处理纯文本消息
      if (typeof msg.content === 'string') {
        return {
          role: msg.role,
          content: msg.content
        };
      }
      // 处理包含图片的复合消息（用于视觉模型）
      else if (Array.isArray(msg.content)) {
        return {
          role: msg.role,
          content: msg.content
        };
      }
      // 默认情况
      return {
        role: msg.role,
        content: msg.content
      };
    });

    // 调用LLM API
    let response = '';
    try {
      const { answer, cot } = await llmClient.getResponseWithCOT(formattedMessages);
      response = `<think>${cot}</think>${answer}`;
    } catch (error) {
      console.error('Failed to call LLM API:', String(error));
      return NextResponse.json(
        {
          error: `Failed to call ${model.provider} model: ${error.message}`
        },
        { status: 500 }
      );
    }

    return NextResponse.json({ response });
  } catch (error) {
    console.error('Failed to process chat request:', String(error));
    return NextResponse.json({ error: `Failed to process chat request: ${error.message}` }, { status: 500 });
  }
}

'use client';

import React from 'react';
import { Stack, LinearProgress, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';

// 任务进度组件
export default function TaskProgress({ task }) {
  const { t } = useTranslation();

  // 如果没有总数，则不显示进度条
  if (task.totalCount === 0) return '-';

  // 计算进度百分比
  const progress = (task.completedCount / task.totalCount) * 100;

  return (
    <Stack direction="column" spacing={0.5}>
      <LinearProgress
        variant="determinate"
        value={progress}
        sx={{
          height: 6,
          borderRadius: 3,
          width: 120,
          '& .MuiLinearProgress-bar': {
            transition: 'transform 0.5s ease'
          }
        }}
      />
      <Typography variant="caption" color="text.secondary">
        {task.completedCount} / {task.totalCount} ({Math.round(progress)}%)
      </Typography>
    </Stack>
  );
}

import { NextResponse } from 'next/server';
import LLMClient from '@/lib/llm/core/index';

/**
 * 流式输出的聊天接口
 */
export async function POST(request, { params }) {
  const { projectId } = params;

  try {
    const body = await request.json();
    const { model, messages } = body;

    if (!model || !messages) {
      return NextResponse.json({ error: 'Missing necessary parameters' }, { status: 400 });
    }

    // 创建 LLM 客户端
    const llmClient = new LLMClient(model);

    // 格式化消息历史
    const formattedMessages = messages.map(msg => {
      // 处理纯文本消息
      if (typeof msg.content === 'string') {
        return {
          role: msg.role,
          content: msg.content
        };
      }
      // 处理包含图片的复合消息（用于视觉模型）
      else if (Array.isArray(msg.content)) {
        return {
          role: msg.role,
          content: msg.content
        };
      }
      // 默认情况
      return {
        role: msg.role,
        content: msg.content
      };
    });

    try {
      // 调用纯API流式输出
      const response = await llmClient.chatStreamAPI(formattedMessages);
      // 返回流式响应
      return response;
    } catch (error) {
      console.error('Failed to call LLM API:', error);
      return NextResponse.json(
        {
          error: `Failed to call ${model.provider} model: ${error.message}`
        },
        { status: 500 }
      );
    }
  } catch (error) {
    console.error('Failed to process stream chat request:', String(error));
    return NextResponse.json({ error: `Failed to process stream chat request: ${error.message}` }, { status: 500 });
  }
}

/**
 * Markdown文档分割模块
 */

/**
 * 分割超长段落
 * @param {Object} section - 段落对象
 * @param {number} maxSplitLength - 最大分割字数
 * @returns {Array} - 分割后的段落数组
 */
function splitLongSection(section, maxSplitLength) {
  const content = section.content;
  const paragraphs = content.split(/\n\n+/);
  const result = [];
  let currentChunk = '';

  for (const paragraph of paragraphs) {
    // 如果当前段落本身超过最大长度，可能需要进一步拆分
    if (paragraph.length > maxSplitLength) {
      // 如果当前块不为空，先加入结果
      if (currentChunk.length > 0) {
        result.push(currentChunk);
        currentChunk = '';
      }

      // 对超长段落进行分割（例如，按句子或固定长度）
      const sentenceSplit = paragraph.match(/[^.!?。！？]+[.!?。！？]+/g) || [paragraph];

      // 处理分割后的句子
      let sentenceChunk = '';
      for (const sentence of sentenceSplit) {
        if ((sentenceChunk + sentence).length <= maxSplitLength) {
          sentenceChunk += sentence;
        } else {
          if (sentenceChunk.length > 0) {
            result.push(sentenceChunk);
          }
          // 如果单个句子超过最大长度，可能需要进一步拆分
          if (sentence.length > maxSplitLength) {
            // 简单地按固定长度分割
            for (let i = 0; i < sentence.length; i += maxSplitLength) {
              result.push(sentence.substr(i, maxSplitLength));
            }
          } else {
            sentenceChunk = sentence;
          }
        }
      }

      if (sentenceChunk.length > 0) {
        currentChunk = sentenceChunk;
      }
    } else if ((currentChunk + '\n\n' + paragraph).length <= maxSplitLength) {
      // 如果添加当前段落不超过最大长度，则添加到当前块
      currentChunk = currentChunk.length > 0 ? currentChunk + '\n\n' + paragraph : paragraph;
    } else {
      // 如果添加当前段落超过最大长度，则将当前块加入结果，并重新开始一个新块
      result.push(currentChunk);
      currentChunk = paragraph;
    }
  }

  // 添加最后一个块（如果有）
  if (currentChunk.length > 0) {
    result.push(currentChunk);
  }

  return result;
}

/**
 * 处理段落，根据最小和最大分割字数进行分割
 * @param {Array} sections - 段落数组
 * @param {Array} outline - 目录大纲
 * @param {number} minSplitLength - 最小分割字数
 * @param {number} maxSplitLength - 最大分割字数
 * @returns {Array} - 处理后的段落数组
 */
function processSections(sections, outline, minSplitLength, maxSplitLength) {
  // 预处理：将相邻的小段落合并
  const preprocessedSections = [];
  let currentSection = null;

  for (const section of sections) {
    const contentLength = section.content.trim().length;

    if (contentLength < minSplitLength && currentSection) {
      // 如果当前段落小于最小长度且有累积段落，尝试合并
      const mergedContent = `${currentSection.content}\n\n${section.heading ? `${'#'.repeat(section.level)} ${section.heading}\n` : ''}${section.content}`;

      if (mergedContent.length <= maxSplitLength) {
        // 如果合并后不超过最大长度，则合并
        currentSection.content = mergedContent;
        if (section.heading) {
          currentSection.headings = currentSection.headings || [];
          currentSection.headings.push({
            heading: section.heading,
            level: section.level,
            position: section.position
          });
        }
        continue;
      }
    }

    // 如果无法合并，则开始新的段落
    if (currentSection) {
      preprocessedSections.push(currentSection);
    }
    currentSection = {
      ...section,
      headings: section.heading ? [{ heading: section.heading, level: section.level, position: section.position }] : []
    };
  }

  // 添加最后一个段落
  if (currentSection) {
    preprocessedSections.push(currentSection);
  }

  const result = [];
  let accumulatedSection = null; // 用于累积小于最小分割字数的段落

  for (let i = 0; i < preprocessedSections.length; i++) {
    const section = preprocessedSections[i];
    const contentLength = section.content.trim().length;

    // 检查是否需要累积段落
    if (contentLength < minSplitLength) {
      // 如果还没有累积过段落，创建新的累积段落
      if (!accumulatedSection) {
        accumulatedSection = {
          heading: section.heading,
          level: section.level,
          content: section.content,
          position: section.position,
          headings: [{ heading: section.heading, level: section.level, position: section.position }]
        };
      } else {
        // 已经有累积段落，将当前段落添加到累积段落中
        accumulatedSection.content += `\n\n${section.heading ? `${'#'.repeat(section.level)} ${section.heading}\n` : ''}${section.content}`;
        if (section.heading) {
          accumulatedSection.headings.push({
            heading: section.heading,
            level: section.level,
            position: section.position
          });
        }
      }

      // 只有当累积内容达到最小长度时才处理
      const accumulatedLength = accumulatedSection.content.trim().length;
      if (accumulatedLength >= minSplitLength) {
        const summary = require('./summary').generateEnhancedSummary(accumulatedSection, outline);

        if (accumulatedLength > maxSplitLength) {
          // 如果累积段落超过最大长度，进一步分割
          const subSections = splitLongSection(accumulatedSection, maxSplitLength);

          for (let j = 0; j < subSections.length; j++) {
            result.push({
              summary: `${summary} - Part ${j + 1}/${subSections.length}`,
              content: subSections[j]
            });
          }
        } else {
          // 添加到结果中
          result.push({
            summary,
            content: accumulatedSection.content
          });
        }

        accumulatedSection = null; // 重置累积段落
      }

      continue;
    }

    // 如果有累积的段落，先处理它
    if (accumulatedSection) {
      const summary = require('./summary').generateEnhancedSummary(accumulatedSection, outline);
      const accumulatedLength = accumulatedSection.content.trim().length;

      if (accumulatedLength > maxSplitLength) {
        // 如果累积段落超过最大长度，进一步分割
        const { result: subSections, lastChunk } = splitLongSection(accumulatedSection, maxSplitLength, minSplitLength);

        for (let j = 0; j < subSections.length; j++) {
          result.push({
            summary: `${summary} - Part ${j + 1}/${subSections.length}`,
            content: subSections[j]
          });
        }

        // 如果有未处理的小段落，保存下来等待下一次合并
        if (lastChunk) {
          accumulatedSection = {
            ...accumulatedSection,
            content: lastChunk
          };
          continue;
        }
      } else {
        // 添加到结果中
        result.push({
          summary,
          content: accumulatedSection.content
        });
      }

      accumulatedSection = null; // 重置累积段落
    }

    // 处理当前段落
    // 如果段落长度超过最大分割字数，需要进一步分割
    if (contentLength > maxSplitLength) {
      const subSections = splitLongSection(section, maxSplitLength);

      // 为当前段落创建一个标准的headings数组
      if (!section.headings && section.heading) {
        section.headings = [{ heading: section.heading, level: section.level, position: section.position }];
      }

      for (let i = 0; i < subSections.length; i++) {
        const subSection = subSections[i];
        const summary = require('./summary').generateEnhancedSummary(section, outline, i + 1, subSections.length);

        result.push({
          summary,
          content: subSection
        });
      }
    } else {
      // 为当前段落创建一个标准的headings数组
      if (!section.headings && section.heading) {
        section.headings = [{ heading: section.heading, level: section.level, position: section.position }];
      }

      // 生成增强的摘要并添加到结果
      const summary = require('./summary').generateEnhancedSummary(section, outline);

      const content = `${section.heading ? `${'#'.repeat(section.level)} ${section.heading}\n` : ''}${section.content}`;

      result.push({
        summary,
        content
      });
    }
  }

  // 处理最后剩余的小段落
  if (accumulatedSection) {
    if (result.length > 0) {
      // 尝试将剩余的小段落与最后一个结果合并
      const lastResult = result[result.length - 1];
      const mergedContent = `${lastResult.content}\n\n${accumulatedSection.content}`;

      if (mergedContent.length <= maxSplitLength) {
        // 如果合并后不超过最大长度，则合并
        const summary = require('./summary').generateEnhancedSummary(
          {
            ...accumulatedSection,
            content: mergedContent
          },
          outline
        );

        result[result.length - 1] = {
          summary,
          content: mergedContent
        };
      } else {
        // 如果合并后超过最大长度，将accumulatedSection作为单独的段落添加，这里的contentLength一定小于maxSplitLength
        const summary = require('./summary').generateEnhancedSummary(accumulatedSection, outline);
        const content = `${accumulatedSection.heading ? `${'#'.repeat(accumulatedSection.level)} ${accumulatedSection.heading}\n` : ''}${accumulatedSection.content}`;
        result.push({
          summary,
          content
        });
      }
    } else {
      // 如果result为空，直接添加accumulatedSection
      const summary = require('./summary').generateEnhancedSummary(accumulatedSection, outline);
      const content = `${accumulatedSection.heading ? `${'#'.repeat(accumulatedSection.level)} ${accumulatedSection.heading}\n` : ''}${accumulatedSection.content}`;
      result.push({
        summary,
        content
      });
    }
  }

  return result;
}

module.exports = {
  splitLongSection,
  processSections
};

import { NextResponse } from 'next/server';
import {
  getAllQuestionsByProjectId,
  getQuestions,
  getQuestionsIds,
  isExistByQuestion,
  saveQuestions,
  updateQuestion
} from '@/lib/db/questions';

// 获取项目的所有问题
export async function GET(request, { params }) {
  try {
    const { projectId } = params;
    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: 'Missing project ID' }, { status: 400 });
    }
    const { searchParams } = new URL(request.url);
    let status = searchParams.get('status');
    let answered = undefined;
    if (status === 'answered') answered = true;
    if (status === 'unanswered') answered = false;
    let selectedAll = searchParams.get('selectedAll');
    if (selectedAll) {
      let data = await getQuestionsIds(projectId, answered, searchParams.get('input'));
      return NextResponse.json(data);
    }
    let all = searchParams.get('all');
    if (all) {
      let data = await getAllQuestionsByProjectId(projectId);
      return NextResponse.json(data);
    }
    // 获取问题列表
    const questions = await getQuestions(
      projectId,
      parseInt(searchParams.get('page')),
      parseInt(searchParams.get('size')),
      answered,
      searchParams.get('input')
    );

    return NextResponse.json(questions);
  } catch (error) {
    console.error('Failed to get questions:', String(error));
    return NextResponse.json({ error: error.message || 'Failed to get questions' }, { status: 500 });
  }
}

// 新增问题
export async function POST(request, { params }) {
  try {
    const { projectId } = params;
    const body = await request.json();
    const { question, chunkId, label } = body;

    // 验证必要参数
    if (!projectId || !question || !chunkId) {
      return NextResponse.json({ error: 'Missing necessary parameters' }, { status: 400 });
    }

    // 检查问题是否已存在
    const existingQuestion = await isExistByQuestion(question);
    if (existingQuestion) {
      return NextResponse.json({ error: 'Question already exists' }, { status: 400 });
    }

    // 添加新问题
    let questions = [
      {
        chunkId: chunkId,
        question: question,
        label: label || 'other'
      }
    ];
    // 保存更新后的数据
    let data = await saveQuestions(projectId, questions);

    // 返回成功响应
    return NextResponse.json(data);
  } catch (error) {
    console.error('Failed to create question:', String(error));
    return NextResponse.json({ error: error.message || 'Failed to create question' }, { status: 500 });
  }
}

// 更新问题
export async function PUT(request) {
  try {
    const body = await request.json();
    // 保存更新后的数据
    let data = await updateQuestion(body);
    // 返回更新后的问题数据
    return NextResponse.json(data);
  } catch (error) {
    console.error('更新问题失败:', String(error));
    return NextResponse.json({ error: error.message || '更新问题失败' }, { status: 500 });
  }
}

import React, { useState, useEffect } from 'react';
import { Box, Button, Snackbar, Alert, Typography, Link, CircularProgress, LinearProgress } from '@mui/material';
import UpdateIcon from '@mui/icons-material/Update';
import { useTranslation } from 'react-i18next';

const UpdateChecker = () => {
  const { t } = useTranslation();
  const [updateAvailable, setUpdateAvailable] = useState(false);
  const [updateInfo, setUpdateInfo] = useState(null);
  const [open, setOpen] = useState(false);
  const [checking, setChecking] = useState(false);
  const [downloading, setDownloading] = useState(false);
  const [downloadProgress, setDownloadProgress] = useState(0);
  const [updateDownloaded, setUpdateDownloaded] = useState(false);
  const [updateError, setUpdateError] = useState(null);

  // 检查更新
  const checkForUpdates = async () => {
    if (!window.electron?.updater) {
      console.warn('Update feature is not available, possibly running in browser environment');
      return;
    }

    try {
      setChecking(true);
      setUpdateError(null);

      const result = await window.electron.updater.checkForUpdates();
      console.log('Update check result:', result);

      // 返回当前版本信息
      if (result) {
        setUpdateInfo(prev => ({
          ...prev,
          currentVersion: result.currentVersion
        }));
      }
    } catch (error) {
      console.error('Failed to check for updates:', error);
      // setUpdateError(error.message || 'Failed to check for updates');
    } finally {
      setChecking(false);
    }
  };

  // 下载更新
  const downloadUpdate = async () => {
    if (!window.electron?.updater) return;

    try {
      setDownloading(true);
      setUpdateError(null);
      await window.electron.updater.downloadUpdate();
    } catch (error) {
      console.error('下载更新失败:', error);
      setUpdateError(error.message || '下载更新失败');
      setDownloading(false);
    }
  };

  // 安装更新
  const installUpdate = async () => {
    if (!window.electron?.updater) return;

    try {
      await window.electron.updater.installUpdate();
    } catch (error) {
      console.error('Failed to install update:', error);
      // setUpdateError(error.message || 'Failed to install update');
    }
  };

  // 设置更新事件监听
  useEffect(() => {
    if (!window.electron?.updater) return;

    // 有可用更新
    const removeUpdateAvailable = window.electron.updater.onUpdateAvailable(info => {
      console.log('发现新版本:', info);
      setUpdateAvailable(true);
      setUpdateInfo(prev => ({
        ...prev,
        ...info,
        releaseUrl: `https://github.com/ConardLi/easy-dataset/releases`
      }));
      setOpen(true);
    });

    // 没有可用更新
    const removeUpdateNotAvailable = window.electron.updater.onUpdateNotAvailable(() => {
      console.log('没有可用更新');
      setUpdateAvailable(false);
    });

    // 更新错误
    const removeUpdateError = window.electron.updater.onUpdateError(error => {
      console.error('更新错误:', error);
      // setUpdateError(error);
    });

    // 下载进度
    const removeDownloadProgress = window.electron.updater.onDownloadProgress(progress => {
      console.log('下载进度:', progress);
      setDownloadProgress(progress.percent || 0);
    });

    // 更新下载完成
    const removeUpdateDownloaded = window.electron.updater.onUpdateDownloaded(info => {
      console.log('更新下载完成:', info);
      setDownloading(false);
      setUpdateDownloaded(true);
    });

    // 组件挂载时检查更新
    const timer = setTimeout(() => {
      checkForUpdates();
    }, 5000);

    // 清理函数
    return () => {
      clearTimeout(timer);
      removeUpdateAvailable();
      removeUpdateNotAvailable();
      removeUpdateError();
      removeDownloadProgress();
      removeUpdateDownloaded();
    };
  }, []);

  // 定期检查更新（每小时一次）
  useEffect(() => {
    if (!window.electron?.updater) return;

    const interval = setInterval(
      () => {
        checkForUpdates();
      },
      60 * 60 * 1000
    );

    return () => clearInterval(interval);
  }, []);

  const handleClose = () => {
    setOpen(false);
  };

  // 如果没有更新或者不在 Electron 环境中，不显示任何内容
  if (!updateAvailable && !open) return null;

  return (
    <>
      {updateAvailable && (
        <Button color="primary" startIcon={<UpdateIcon />} onClick={() => setOpen(true)} sx={{ ml: 1 }}>
          {t('update.newVersion')}
        </Button>
      )}

      <Snackbar
        open={open}
        autoHideDuration={null}
        onClose={handleClose}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
      >
        <Alert onClose={handleClose} severity="info" sx={{ width: '100%', maxWidth: 400 }}>
          <Box sx={{ p: 1 }}>
            <Typography variant="h6">{t('update.newVersionAvailable')}</Typography>

            {updateInfo && (
              <>
                <Typography variant="body2" sx={{ mt: 1 }}>
                  {t('update.currentVersion')}: {updateInfo.currentVersion}
                </Typography>
                <Typography variant="body2">
                  {t('update.latestVersion')}: {updateInfo.version}
                </Typography>
              </>
            )}

            {checking && (
              <Box sx={{ display: 'flex', alignItems: 'center', mt: 1 }}>
                <CircularProgress size={16} sx={{ mr: 1 }} />
                <Typography variant="body2">{t('update.checking')}</Typography>
              </Box>
            )}

            {updateError && (
              <Typography variant="body2" color="error.main" sx={{ mt: 1 }}>
                {updateError}
              </Typography>
            )}

            {downloading && (
              <Box sx={{ mt: 2, width: '100%' }}>
                <Typography variant="body2" sx={{ mb: 0.5 }}>
                  {t('update.downloading')}: {Math.round(downloadProgress)}%
                </Typography>
                <LinearProgress variant="determinate" value={downloadProgress} />
              </Box>
            )}

            <Box sx={{ mt: 2, display: 'flex', gap: 2 }}>
              {/* {!downloading && !updateDownloaded ? (
                <Button
                  variant="contained"
                  color="primary"
                  disabled={checking || downloading}
                  onClick={downloadUpdate}
                >
                  {t('update.downloadNow')}
                </Button>
              ) : updateDownloaded ? (
                <Button
                  variant="contained"
                  color="primary"
                  onClick={installUpdate}
                >
                  {t('update.installNow')}
                </Button>
              ) : null} */}

              {updateInfo?.releaseUrl && (
                <Link href={updateInfo.releaseUrl} target="_blank" rel="noopener noreferrer">
                  <Button variant="outlined">{t('update.viewRelease')}</Button>
                </Link>
              )}
            </Box>
          </Box>
        </Alert>
      </Snackbar>
    </>
  );
};

export default UpdateChecker;

import TurndownService from 'turndown';
import mammoth from 'mammoth';

/**
 * 获取文件内容
 * @param {*} file
 */
export async function getContent(file) {
  let fileContent;
  let fileName = file.name;

  // 如果是 docx 文件，先转换为 markdown
  if (file.name.endsWith('.docx')) {
    const arrayBuffer = await file.arrayBuffer();
    const htmlResult = await mammoth.convertToHtml(
      { arrayBuffer },
      {
        convertImage: image => {
          return mammoth.docx.paragraph({
            children: [
              mammoth.docx.textRun({
                text: ''
              })
            ]
          });
        }
      }
    );
    const turndownService = new TurndownService();
    fileContent = turndownService.turndown(htmlResult.value);
    fileName = file.name.replace('.docx', '.md');
  } else {
    // 对于 md 和 txt 文件，直接读取内容
    const reader = new FileReader();
    fileContent = await new Promise((resolve, reject) => {
      reader.onload = () => resolve(reader.result);
      reader.onerror = reject;
      reader.readAsArrayBuffer(file);
    });
    fileName = file.name.replace('.txt', '.md');
  }
  return { fileContent, fileName };
}

import { NextResponse } from 'next/server';
import path from 'path';
import fs from 'fs/promises';
import { getProjectRoot } from '@/lib/db/base';

// 获取模型配置
export async function GET(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目 ID
    if (!projectId) {
      return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
    }

    // 获取项目根目录
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);

    // 检查项目是否存在
    try {
      await fs.access(projectPath);
    } catch (error) {
      return NextResponse.json({ error: 'The project does not exist' }, { status: 404 });
    }

    // 获取模型配置文件路径
    const modelConfigPath = path.join(projectPath, 'model-config.json');

    // 检查模型配置文件是否存在
    try {
      await fs.access(modelConfigPath);
    } catch (error) {
      // 如果配置文件不存在，返回默认配置
      return NextResponse.json([]);
    }

    // 读取模型配置文件
    const modelConfigData = await fs.readFile(modelConfigPath, 'utf-8');
    const modelConfig = JSON.parse(modelConfigData);

    return NextResponse.json(modelConfig);
  } catch (error) {
    console.error('Error obtaining model configuration:', String(error));
    return NextResponse.json({ error: 'Failed to obtain model configuration' }, { status: 500 });
  }
}

// 更新模型配置
export async function PUT(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目 ID
    if (!projectId) {
      return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
    }

    // 获取请求体
    const modelConfig = await request.json();

    // 验证请求体
    if (!modelConfig || !Array.isArray(modelConfig)) {
      return NextResponse.json({ error: 'The model configuration must be an array' }, { status: 400 });
    }

    // 获取项目根目录
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);

    // 检查项目是否存在
    try {
      await fs.access(projectPath);
    } catch (error) {
      return NextResponse.json({ error: 'The project does not exist' }, { status: 404 });
    }

    // 获取模型配置文件路径
    const modelConfigPath = path.join(projectPath, 'model-config.json');

    // 写入模型配置文件
    await fs.writeFile(modelConfigPath, JSON.stringify(modelConfig, null, 2), 'utf-8');

    return NextResponse.json({ message: 'Model configuration updated successfully' });
  } catch (error) {
    console.error('Error updating model configuration:', String(error));
    return NextResponse.json({ error: 'Failed to update model configuration' }, { status: 500 });
  }
}

'use client';

import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
  Box,
  Typography,
  Checkbox,
  IconButton,
  Chip,
  Tooltip,
  Pagination,
  Divider,
  Paper,
  CircularProgress,
  Button,
  TextField
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import EditIcon from '@mui/icons-material/Edit';
import { useGenerateDataset } from '@/hooks/useGenerateDataset';

export default function QuestionListView({
  questions = [],
  currentPage,
  totalQuestions = 0,
  handlePageChange,
  selectedQuestions = [],
  onSelectQuestion,
  onDeleteQuestion,
  projectId,
  onEditQuestion,
  refreshQuestions
}) {
  const { t } = useTranslation();
  // 处理状态
  const [processingQuestions, setProcessingQuestions] = useState({});
  const { generateSingleDataset } = useGenerateDataset();

  // 获取文本块的标题
  const getChunkTitle = content => {
    const firstLine = content.split('\n')[0].trim();
    if (firstLine.startsWith('# ')) {
      return firstLine.substring(2);
    } else if (firstLine.length > 0) {
      return firstLine.length > 200 ? firstLine.substring(0, 200) + '...' : firstLine;
    }
    return t('chunks.defaultTitle');
  };

  // 检查问题是否被选中
  const isQuestionSelected = questionId => {
    return selectedQuestions.includes(questionId);
  };

  // 处理生成数据集
  const handleGenerateDataset = async (questionId, questionInfo) => {
    // 设置处理状态
    setProcessingQuestions(prev => ({
      ...prev,
      [questionId]: true
    }));
    await generateSingleDataset({ projectId, questionId, questionInfo });
    // 重置处理状态
    setProcessingQuestions(prev => ({
      ...prev,
      [questionId]: false
    }));
    refreshQuestions();
  };

  return (
    <Box style={{ padding: '20px' }}>
      {/* 问题列表 */}
      <Paper
        elevation={0}
        sx={{
          borderRadius: 2,
          overflow: 'hidden',
          boxShadow: '0 2px 4px rgba(0,0,0,0.05)'
        }}
      >
        <Box sx={{ px: 2, py: 1, display: 'flex', alignItems: 'center', bgcolor: 'background.paper' }}>
          <Typography variant="body2" sx={{ fontWeight: 500, ml: 1 }}>
            {t('datasets.question')}
          </Typography>
          <Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center' }}>
            <Typography variant="body2" sx={{ fontWeight: 500, mr: 2, display: { xs: 'none', sm: 'block' } }}>
              {t('common.label')}
            </Typography>
            <Typography
              variant="body2"
              sx={{ fontWeight: 500, width: 150, mr: 2, display: { xs: 'none', md: 'block' } }}
            >
              {t('chunks.title')}
            </Typography>
            <Typography variant="body2" sx={{ fontWeight: 500, width: 100, textAlign: 'center' }}>
              {t('common.actions')}
            </Typography>
          </Box>
        </Box>

        <Divider />

        {questions.map((question, index) => {
          const isSelected = isQuestionSelected(question.id);
          const questionKey = question.id;
          return (
            <Box key={questionKey}>
              <Box
                sx={{
                  px: 2,
                  py: 1.5,
                  display: 'flex',
                  alignItems: 'center',
                  bgcolor: isSelected ? 'action.selected' : 'background.paper',
                  '&:hover': {
                    bgcolor: 'action.hover'
                  }
                }}
              >
                <Checkbox
                  checked={isSelected}
                  onChange={() => {
                    onSelectQuestion(questionKey);
                  }}
                  size="small"
                />

                <Box sx={{ ml: 1, flex: 1, mr: 2 }}>
                  <Typography variant="body2">
                    {question.question}
                    {question.datasetCount > 0 ? (
                      <Chip
                        label={t('datasets.answerCount', { count: question.datasetCount })}
                        size="small"
                        color="primary"
                        variant="outlined"
                        sx={{ fontSize: '0.75rem', maxWidth: 150 }}
                      />
                    ) : null}
                  </Typography>
                  <Typography variant="caption" color="text.secondary" sx={{ display: { xs: 'block', sm: 'none' } }}>
                    {question.label || t('datasets.noTag')} • ID: {(question.question || '').substring(0, 8)}
                  </Typography>
                </Box>

                <Box sx={{ display: { xs: 'none', sm: 'block' }, mr: 2 }}>
                  {question.label ? (
                    <Chip
                      label={question.label}
                      size="small"
                      color="primary"
                      variant="outlined"
                      sx={{ fontSize: '0.75rem', maxWidth: 150 }}
                    />
                  ) : (
                    <Typography variant="caption" color="text.disabled">
                      {t('datasets.noTag')}
                    </Typography>
                  )}
                </Box>

                <Box sx={{ width: 150, mr: 2, display: { xs: 'none', md: 'block' } }}>
                  <Tooltip title={getChunkTitle(question.chunk.content)}>
                    <Chip
                      label={question.chunk.name}
                      size="small"
                      variant="outlined"
                      color="info"
                      sx={{
                        fontSize: '0.75rem',
                        maxWidth: '100%',
                        textOverflow: 'ellipsis'
                      }}
                    />
                  </Tooltip>
                </Box>

                <Box sx={{ width: 120, display: 'flex', justifyContent: 'center' }}>
                  <Tooltip title={t('common.edit')}>
                    <IconButton
                      size="small"
                      color="primary"
                      onClick={() =>
                        onEditQuestion({
                          id: question.id,
                          question: question.question,
                          chunkId: question.chunkId,
                          label: question.label || 'other'
                        })
                      }
                      disabled={processingQuestions[questionKey]}
                    >
                      <EditIcon fontSize="small" />
                    </IconButton>
                  </Tooltip>
                  <Tooltip title={t('datasets.generateDataset')}>
                    <IconButton
                      size="small"
                      color="primary"
                      onClick={() => handleGenerateDataset(question.id, question.question)}
                      disabled={processingQuestions[questionKey]}
                    >
                      {processingQuestions[questionKey] ? (
                        <CircularProgress size={16} />
                      ) : (
                        <AutoFixHighIcon fontSize="small" />
                      )}
                    </IconButton>
                  </Tooltip>
                  <Tooltip title={t('common.delete')}>
                    <IconButton
                      size="small"
                      color="error"
                      onClick={() => onDeleteQuestion(question.id)}
                      disabled={processingQuestions[questionKey]}
                    >
                      <DeleteIcon fontSize="small" />
                    </IconButton>
                  </Tooltip>
                </Box>
              </Box>
              {index < questions.length - 1 && <Divider />}
            </Box>
          );
        })}
      </Paper>

      {/* 分页 */}
      {totalQuestions > 1 && (
        <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', mt: 3, mb: 2 }}>
          <Pagination
            count={totalQuestions}
            page={currentPage}
            onChange={handlePageChange}
            color="primary"
            showFirstButton
            showLastButton
            shape="rounded"
            size="medium"
          />
          <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
            <Typography variant="body2">{t('common.jumpTo')}:</Typography>
            <TextField
              size="small"
              type="number"
              inputProps={{
                min: 1,
                max: totalQuestions,
                style: { padding: '4px 8px', width: '50px' }
              }}
              onKeyPress={e => {
                if (e.key === 'Enter') {
                  const pageNum = parseInt(e.target.value, 10);
                  if (pageNum >= 1 && pageNum <= totalQuestions) {
                    handlePageChange(null, pageNum);
                    e.target.value = '';
                  }
                }
              }}
            />
          </Box>
        </Box>
      )}
    </Box>
  );
}

import { NextResponse } from 'next/server';
import { db } from '@/lib/db';

/**
 * 根据标签ID获取问题列表
 */
export async function GET(request, { params }) {
  try {
    const { projectId } = params;
    const { searchParams } = new URL(request.url);
    const tagId = searchParams.get('tagId');

    console.log('[distill/questions/by-tag] 请求参数:', { projectId, tagId });

    // 验证参数
    if (!projectId) {
      return NextResponse.json({ error: '项目ID不能为空' }, { status: 400 });
    }

    if (!tagId) {
      return NextResponse.json({ error: '标签ID不能为空' }, { status: 400 });
    }

    // 获取标签信息
    const tag = await db.tags.findUnique({
      where: { id: tagId }
    });

    if (!tag) {
      return NextResponse.json({ error: '标签不存在' }, { status: 404 });
    }

    console.log('[distill/questions/by-tag] 标签信息:', tag);

    // 获取或创建蒸馏文本块
    let distillChunk = await db.chunks.findFirst({
      where: {
        projectId,
        name: 'Distilled Content'
      }
    });

    if (!distillChunk) {
      // 创建一个特殊的蒸馏文本块
      distillChunk = await db.chunks.create({
        data: {
          name: 'Distilled Content',
          projectId,
          fileId: 'distilled',
          fileName: 'distilled.md',
          content:
            'This text block is used to store questions generated through data distillation and is not related to actual literature.',
          summary: 'Questions generated through data distillation',
          size: 0
        }
      });
    }
    const questions = await db.questions.findMany({
      where: {
        projectId,
        label: tag.label,
        chunkId: distillChunk.id
      }
    });

    return NextResponse.json(questions);
  } catch (error) {
    console.error('[distill/questions/by-tag] 获取问题失败:', String(error));
    return NextResponse.json({ error: error.message || '获取问题失败' }, { status: 500 });
  }
}

import { getProjectRoot } from '@/lib/db/base';
import { NextResponse } from 'next/server';
import path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

/**
 * 打开项目目录
 * @returns {Promise<Response>} 操作结果响应
 */
export async function POST(request) {
  try {
    const { projectId } = await request.json();

    if (!projectId) {
      return NextResponse.json(
        {
          success: false,
          error: '项目ID不能为空'
        },
        { status: 400 }
      );
    }

    // 获取项目根目录
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);

    // 根据操作系统打开目录
    const platform = process.platform;
    let command;

    if (platform === 'win32') {
      // Windows
      command = `explorer "${projectPath}"`;
    } else if (platform === 'darwin') {
      // macOS
      command = `open "${projectPath}"`;
    } else {
      // Linux 和其他系统
      command = `xdg-open "${projectPath}"`;
    }

    await execAsync(command);

    return NextResponse.json({
      success: true,
      message: '已打开项目目录'
    });
  } catch (error) {
    console.error('打开项目目录出错:', String(error));
    return NextResponse.json(
      {
        success: false,
        error: error.message
      },
      { status: 500 }
    );
  }
}

'use client';

import { useState, useEffect } from 'react';
import {
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  Button,
  TextField,
  Box,
  Typography,
  useTheme,
  CircularProgress,
  FormControl,
  InputLabel,
  Select,
  MenuItem
} from '@mui/material';
import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';

export default function CreateProjectDialog({ open, onClose }) {
  const { t } = useTranslation();
  const theme = useTheme();
  const router = useRouter();
  const [loading, setLoading] = useState(false);
  const [projects, setProjects] = useState([]);
  const [formData, setFormData] = useState({
    name: '',
    description: '',
    reuseConfigFrom: ''
  });
  const [error, setError] = useState(null);

  // 获取项目列表
  useEffect(() => {
    const fetchProjects = async () => {
      try {
        const response = await fetch('/api/projects');
        if (response.ok) {
          const data = await response.json();
          setProjects(data);
        }
      } catch (error) {
        console.error('获取项目列表失败:', error);
      }
    };

    fetchProjects();
  }, []);

  const handleChange = e => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  const handleSubmit = async e => {
    e.preventDefault();
    setLoading(true);
    setError(null);

    try {
      const response = await fetch('/api/projects', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(formData)
      });

      if (!response.ok) {
        throw new Error(t('projects.createFailed'));
      }

      const data = await response.json();

      router.push(`/projects/${data.id}/settings?tab=model`);
    } catch (err) {
      console.error(t('projects.createError'), err);
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <Dialog
      open={open}
      onClose={onClose}
      maxWidth="sm"
      fullWidth
      PaperProps={{
        sx: {
          borderRadius: '16px',
          background: theme.palette.mode === 'dark' ? 'rgba(30, 30, 30, 0.9)' : 'rgba(255, 255, 255, 0.9)',
          backdropFilter: 'blur(8px)'
        }
      }}
    >
      <DialogTitle>
        <Typography variant="h5" fontWeight="bold">
          {t('projects.createNew')}
        </Typography>
      </DialogTitle>
      <form onSubmit={handleSubmit}>
        <DialogContent>
          <Box sx={{ mb: 3 }}>
            <TextField
              name="name"
              label={t('projects.name')}
              fullWidth
              required
              value={formData.name}
              onChange={handleChange}
              sx={{ mb: 2 }}
            />
            <TextField
              name="description"
              label={t('projects.description')}
              fullWidth
              multiline
              rows={4}
              value={formData.description}
              onChange={handleChange}
            />
            <FormControl fullWidth sx={{ mt: 2 }}>
              <InputLabel id="reuse-config-label">{t('projects.reuseConfig')}</InputLabel>
              <Select
                labelId="reuse-config-label"
                name="reuseConfigFrom"
                value={formData.reuseConfigFrom}
                onChange={handleChange}
                label={t('projects.reuseConfig')}
              >
                <MenuItem value="">{t('projects.noReuse')}</MenuItem>
                {projects.map(project => (
                  <MenuItem key={project.id} value={project.id}>
                    {project.name}
                  </MenuItem>
                ))}
              </Select>
            </FormControl>
          </Box>
          {error && (
            <Typography color="error" variant="body2" sx={{ mb: 2 }}>
              {error}
            </Typography>
          )}
        </DialogContent>
        <DialogActions sx={{ px: 3, pb: 3 }}>
          <Button onClick={onClose}>{t('common.cancel')}</Button>
          <Button
            type="submit"
            variant="contained"
            disabled={loading || !formData.name}
            sx={{
              background: theme.palette.gradient.primary,
              '&:hover': {
                boxShadow: '0 8px 16px rgba(0, 0, 0, 0.1)'
              }
            }}
          >
            {loading ? <CircularProgress size={24} /> : t('home.createProject')}
          </Button>
        </DialogActions>
      </form>
    </Dialog>
  );
}

import React, { useState, useEffect } from 'react';
import { Box, TextField, Button, Typography, Alert, Grid, Card, CardContent } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import { useTranslation } from 'react-i18next';
import fetchWithRetry from '@/lib/util/request';
import { useSnackbar } from '@/hooks/useSnackbar';

export default function PromptSettings({ projectId }) {
  const { t } = useTranslation();
  const { showSuccess, showError, SnackbarComponent } = useSnackbar();
  const [prompts, setPrompts] = useState({
    globalPrompt: '',
    questionPrompt: '',
    answerPrompt: '',
    labelPrompt: '',
    domainTreePrompt: ''
  });
  const [loading, setLoading] = useState(false);

  // 加载提示词配置
  useEffect(() => {
    const loadPrompts = async () => {
      try {
        const response = await fetchWithRetry(`/api/projects/${projectId}/config`);
        const config = await response.json();
        // 提取提示词相关的字段
        const promptFields = {
          globalPrompt: config.globalPrompt || '',
          questionPrompt: config.questionPrompt || '',
          answerPrompt: config.answerPrompt || '',
          labelPrompt: config.labelPrompt || '',
          domainTreePrompt: config.domainTreePrompt || ''
        };
        setPrompts(promptFields);
      } catch (error) {
        console.error('加载提示词配置失败:', error);
        showError(t('settings.loadPromptsFailed'));
      }
    };
    loadPrompts();
  }, [projectId]);

  // 保存提示词配置
  const handleSave = async () => {
    setLoading(true);
    try {
      const response = await fetchWithRetry(`/api/projects/${projectId}/config`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ prompts: { ...prompts } })
      });
      const data = await response.json();
      if (response.ok) {
        showSuccess(t('settings.savePromptsSuccess'));
      } else {
        throw new Error(data.error || '保存失败');
      }
    } catch (error) {
      console.error('保存提示词配置失败:', error);
      showError(t('settings.savePromptsFailed'));
    } finally {
      setLoading(false);
    }
  };

  const handleChange = field => event => {
    setPrompts(prev => ({
      ...prev,
      [field]: event.target.value
    }));
  };

  return (
    <Box>
      <Alert severity="info" sx={{ mb: 3 }}>
        {t('settings.promptsDescription')}
      </Alert>
      <SnackbarComponent />
      <Grid container spacing={3}>
        {/* 全局提示词 */}
        <Grid item xs={12}>
          <Card>
            <CardContent>
              <Typography variant="subtitle1" fontWeight="bold" gutterBottom>
                {t('settings.globalPrompt')}
              </Typography>
              <TextField
                fullWidth
                multiline
                rows={3}
                size="small"
                value={prompts.globalPrompt}
                onChange={handleChange('globalPrompt')}
                placeholder={t('settings.globalPromptPlaceholder')}
              />
            </CardContent>
          </Card>
        </Grid>

        {/* 生成问题提示词 */}
        <Grid item xs={12} md={6}>
          <Card>
            <CardContent>
              <Typography variant="subtitle1" fontWeight="bold" gutterBottom>
                {t('settings.questionPrompt')}
              </Typography>
              <TextField
                fullWidth
                multiline
                rows={3}
                size="small"
                value={prompts.questionPrompt}
                onChange={handleChange('questionPrompt')}
                placeholder={t('settings.questionPromptPlaceholder')}
              />
            </CardContent>
          </Card>
        </Grid>

        {/* 生成答案提示词 */}
        <Grid item xs={12} md={6}>
          <Card>
            <CardContent>
              <Typography variant="subtitle1" fontWeight="bold" gutterBottom>
                {t('settings.answerPrompt')}
              </Typography>
              <TextField
                fullWidth
                multiline
                rows={3}
                size="small"
                value={prompts.answerPrompt}
                onChange={handleChange('answerPrompt')}
                placeholder={t('settings.answerPromptPlaceholder')}
              />
            </CardContent>
          </Card>
        </Grid>

        {/* 问题打标提示词 */}
        <Grid item xs={12} md={6}>
          <Card>
            <CardContent>
              <Typography variant="subtitle1" fontWeight="bold" gutterBottom>
                {t('settings.labelPrompt')}
              </Typography>
              <TextField
                disabled
                fullWidth
                multiline
                rows={3}
                size="small"
                value={prompts.labelPrompt}
                onChange={handleChange('labelPrompt')}
                placeholder={t('settings.labelPromptPlaceholder')}
              />
            </CardContent>
          </Card>
        </Grid>

        {/* 构建领域树提示词 */}
        <Grid item xs={12} md={6}>
          <Card>
            <CardContent>
              <Typography variant="subtitle1" fontWeight="bold" gutterBottom>
                {t('settings.domainTreePrompt')}
              </Typography>
              <TextField
                fullWidth
                multiline
                rows={3}
                size="small"
                value={prompts.domainTreePrompt}
                onChange={handleChange('domainTreePrompt')}
                placeholder={t('settings.domainTreePromptPlaceholder')}
              />
            </CardContent>
          </Card>
        </Grid>
      </Grid>

      <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 3 }}>
        <Button variant="contained" onClick={handleSave} disabled={loading} startIcon={<SaveIcon />}>
          {t('common.save')}
        </Button>
      </Box>
    </Box>
  );
}

'use client';

import { useState, useCallback } from 'react';
import { Snackbar, Alert } from '@mui/material';

export const useSnackbar = () => {
  const [open, setOpen] = useState(false);
  const [message, setMessage] = useState('');
  const [severity, setSeverity] = useState('info');

  const showMessage = useCallback((newMessage, newSeverity = 'info') => {
    setMessage(newMessage);
    setSeverity(newSeverity);
    setOpen(true);
  }, []);

  const showSuccess = useCallback(
    message => {
      showMessage(message, 'success');
    },
    [showMessage]
  );

  const showError = useCallback(
    message => {
      showMessage(message, 'error');
    },
    [showMessage]
  );

  const showInfo = useCallback(
    message => {
      showMessage(message, 'info');
    },
    [showMessage]
  );

  const showWarning = useCallback(
    message => {
      showMessage(message, 'warning');
    },
    [showMessage]
  );

  const handleClose = useCallback(() => {
    setOpen(false);
  }, []);

  const SnackbarComponent = useCallback(
    () => (
      <Snackbar
        open={open}
        autoHideDuration={2000}
        onClose={handleClose}
        anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
      >
        <Alert onClose={handleClose} severity={severity}>
          {message}
        </Alert>
      </Snackbar>
    ),
    [open, message, severity, handleClose]
  );

  return {
    showMessage,
    showSuccess,
    showError,
    showInfo,
    showWarning,
    SnackbarComponent
  };
};

'use client';

import { useState, useEffect, useRef } from 'react';
import {
  Box,
  TextField,
  InputAdornment,
  List,
  ListItem,
  ListItemButton,
  ListItemText,
  Paper,
  Typography,
  ClickAwayListener,
  Fade,
  Avatar,
  useTheme,
  alpha
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import LaunchIcon from '@mui/icons-material/Launch';
import TravelExploreIcon from '@mui/icons-material/TravelExplore';
import sites from '@/constant/sites.json';
import { useTranslation } from 'react-i18next';

export function DatasetSearchBar() {
  const [searchQuery, setSearchQuery] = useState('');
  const [showSuggestions, setShowSuggestions] = useState(false);
  const [recentSearches, setRecentSearches] = useState([]);
  const searchRef = useRef(null);
  const suggestionsRef = useRef(null);
  const theme = useTheme();
  const { t } = useTranslation();

  // 从 localStorage 加载最近搜索
  useEffect(() => {
    const savedSearches = localStorage.getItem('recentDatasetSearches');
    if (savedSearches) {
      try {
        const searches = JSON.parse(savedSearches);
        setRecentSearches(searches);
      } catch (e) {
        console.error('解析最近搜索失败', e);
      }
    }
  }, []);

  // 处理搜索输入变化
  const handleSearchChange = event => {
    setSearchQuery(event.target.value);
    if (event.target.value) {
      setShowSuggestions(true);
    } else {
      setShowSuggestions(false);
    }
  };

  // 处理回车搜索
  const handleSearchSubmit = event => {
    if (event.key === 'Enter' && searchQuery.trim()) {
      // 默认使用第一个搜索引擎
      if (sites.length > 0) {
        handleSuggestionClick(sites[0]);
      }
    }
  };

  // 保存最近搜索
  const saveRecentSearch = query => {
    if (!query.trim()) return;

    // 添加到最近搜索并去重
    const updatedSearches = [query, ...recentSearches.filter(s => s !== query)].slice(0, 5);
    setRecentSearches(updatedSearches);

    // 保存到 localStorage
    try {
      localStorage.setItem('recentDatasetSearches', JSON.stringify(updatedSearches));
    } catch (e) {
      console.error('保存最近搜索失败', e);
    }
  };

  // 处理点击搜索建议
  const handleSuggestionClick = site => {
    if (searchQuery.trim()) {
      // 根据不同网站处理搜索参数
      let searchUrl = site.link;

      // 如果链接中不包含问号，则添加搜索参数
      if (site.link.includes('huggingface.co')) {
        searchUrl = `${site.link}?sort=trending&search=${encodeURIComponent(searchQuery)}`;
      } else if (site.link.includes('kaggle.com')) {
        searchUrl = `${site.link}?search=${encodeURIComponent(searchQuery)}`;
      } else if (site.link.includes('datasetsearch.research.google.com')) {
        searchUrl = `${site.link}/search?query=${encodeURIComponent(searchQuery)}&src=0`;
      } else if (site.link.includes('paperswithcode.com')) {
        searchUrl = `${site.link}?q=${encodeURIComponent(searchQuery)}`;
      } else if (site.link.includes('modelscope.cn')) {
        searchUrl = `${site.link}?query=${encodeURIComponent(searchQuery)}`;
      } else if (site.link.includes('opendatalab.com')) {
        searchUrl = `${site.link}?keywords=${encodeURIComponent(searchQuery)}`;
      } else if (site.link.includes('tianchi.aliyun.com')) {
        searchUrl = `${site.link}?q=${encodeURIComponent(searchQuery)}`;
      } else {
        // 默认处理方式，在URL后添加搜索参数
        searchUrl = `${site.link}${site.link.includes('?') ? '&' : '?'}search=${encodeURIComponent(searchQuery)}`;
      }

      // 保存最近搜索
      saveRecentSearch(searchQuery);

      window.open(searchUrl, '_blank');
    }
    setShowSuggestions(false);
  };

  // 处理点击外部关闭建议
  const handleClickAway = event => {
    // 确保点击的不是建议框本身
    if (suggestionsRef.current && !suggestionsRef.current.contains(event.target)) {
      setShowSuggestions(false);
    }
  };

  return (
    <ClickAwayListener onClickAway={handleClickAway}>
      <Box sx={{ position: 'relative', width: '100%', zIndex: 1300 }} ref={searchRef}>
        <TextField
          fullWidth
          placeholder={t('datasetSquare.searchPlaceholder')}
          value={searchQuery}
          onChange={handleSearchChange}
          onKeyDown={handleSearchSubmit}
          onClick={() => searchQuery && setShowSuggestions(true)}
          InputProps={{
            startAdornment: (
              <InputAdornment position="start">
                <SearchIcon color="primary" />
              </InputAdornment>
            ),
            sx: {
              height: 56,
              borderRadius: 3,
              backgroundColor:
                theme.palette.mode === 'dark'
                  ? alpha(theme.palette.background.default, 0.6)
                  : alpha(theme.palette.background.default, 0.8),
              backdropFilter: 'blur(8px)',
              px: 2,
              transition: 'all 0.3s ease',
              boxShadow: `0 0 0 1px ${alpha(theme.palette.primary.main, 0.15)}`,
              '&.MuiOutlinedInput-root': {
                '& fieldset': {
                  borderColor: 'transparent'
                },
                '&:hover fieldset': {
                  borderColor: 'transparent'
                },
                '&.Mui-focused': {
                  boxShadow: `0 0 0 2px ${alpha(theme.palette.primary.main, 0.3)}`,
                  backgroundColor:
                    theme.palette.mode === 'dark'
                      ? alpha(theme.palette.background.paper, 0.8)
                      : alpha(theme.palette.common.white, 0.95)
                },
                '&.Mui-focused fieldset': {
                  borderColor: 'transparent'
                }
              }
            }
          }}
          sx={{
            mb: 1,
            '& .MuiInputBase-input': {
              fontSize: '1rem',
              fontWeight: 500,
              color: theme.palette.text.primary
            },
            '& .MuiInputBase-input::placeholder': {
              color: alpha(theme.palette.text.primary, 0.6),
              opacity: 0.7
            }
          }}
        />

        {/* 搜索建议下拉框 - 使用绝对定位确保不被裁剪 */}
        {showSuggestions && searchQuery && (
          <Box
            ref={suggestionsRef}
            sx={{
              position: 'absolute',
              width: '100%',
              zIndex: 9999,
              top: 'calc(100% + 8px)',
              left: 0,
              boxShadow: '0 4px 20px rgba(0,0,0,0.1)',
              pointerEvents: 'auto' // 确保可以点击
            }}
          >
            <Fade in={showSuggestions}>
              <Paper
                elevation={6}
                sx={{
                  width: '100%',
                  maxHeight: 350,
                  overflow: 'auto',
                  borderRadius: 2,
                  border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
                  position: 'relative'
                }}
              >
                <List>
                  {sites.slice(0, 5).map((site, index) => (
                    <ListItem key={index} disablePadding>
                      <ListItemButton
                        onClick={() => handleSuggestionClick(site)}
                        sx={{
                          py: 1.5,
                          '&:hover': {
                            bgcolor: alpha(theme.palette.primary.main, 0.05)
                          }
                        }}
                      >
                        <ListItemText
                          primary={
                            <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                              <Box sx={{ display: 'flex', alignItems: 'center' }}>
                                <Avatar
                                  sx={{
                                    width: 28,
                                    height: 28,
                                    mr: 1.5,
                                    bgcolor: alpha(theme.palette.primary.main, 0.1),
                                    color: theme.palette.primary.main
                                  }}
                                >
                                  <TravelExploreIcon fontSize="small" />
                                </Avatar>
                                <Typography>
                                  {t('datasetSquare.searchVia')} <strong>{site.name}</strong> Search
                                </Typography>
                              </Box>
                              <Box sx={{ display: 'flex', alignItems: 'center' }}>
                                <Typography variant="body2" color="text.secondary" sx={{ mr: 1 }}>
                                  "{searchQuery}"
                                </Typography>
                                <LaunchIcon fontSize="small" color="action" />
                              </Box>
                            </Box>
                          }
                        />
                      </ListItemButton>
                    </ListItem>
                  ))}
                </List>
              </Paper>
            </Fade>
          </Box>
        )}
      </Box>
    </ClickAwayListener>
  );
}

import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';
import { getProjectRoot } from '@/lib/db/base';
import { getUploadFileInfoById } from '@/lib/db/upload-files';

// 获取文件内容
export async function GET(request, { params }) {
  try {
    const { projectId, fileId } = params;

    // 验证参数
    if (!projectId) {
      return NextResponse.json({ error: 'Project ID cannot be empty' }, { status: 400 });
    }

    // 获取项目根目录
    let fileInfo = await getUploadFileInfoById(fileId);
    if (!fileInfo) {
      return NextResponse.json({ error: 'file does not exist' }, { status: 400 });
    }

    // 获取文件路径
    let filePath = path.join(fileInfo.path, fileInfo.fileName);
    if (fileInfo.fileExt !== '.md') {
      filePath = path.join(fileInfo.path, fileInfo.fileName.replace(/\.[^/.]+$/, '.md'));
    }
    //获取文件
    const buffer = fs.readFileSync(filePath);

    const text = buffer.toString('utf-8');

    return NextResponse.json({
      fileId: fileId,
      fileName: fileInfo.fileName,
      content: text
    });
  } catch (error) {
    console.error('Failed to get text block content:', String(error));
    return NextResponse.json({ error: error.message || 'Failed to get text block content' }, { status: 500 });
  }
}

// LocalExportTab.js 组件
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
  Button,
  FormControl,
  FormControlLabel,
  RadioGroup,
  Radio,
  TextField,
  Checkbox,
  Typography,
  Box,
  Paper,
  useTheme,
  Grid,
  Table,
  TableRow,
  TableHead,
  TableBody,
  TableCell,
  TableContainer
} from '@mui/material';

const LocalExportTab = ({
  fileFormat,
  formatType,
  systemPrompt,
  confirmedOnly,
  includeCOT,
  customFields,
  alpacaFieldType,
  customInstruction,
  handleFileFormatChange,
  handleFormatChange,
  handleSystemPromptChange,
  handleConfirmedOnlyChange,
  handleIncludeCOTChange,
  handleCustomFieldChange,
  handleIncludeLabelsChange,
  handleIncludeChunkChange,
  handleAlpacaFieldTypeChange,
  handleCustomInstructionChange,
  handleExport
}) => {
  const theme = useTheme();
  const { t } = useTranslation();

  // 自定义格式的示例
  const getCustomFormatExample = () => {
    const { questionField, answerField, cotField, includeLabels, includeChunk } = customFields;
    const example = {
      [questionField]: '问题内容',
      [answerField]: '答案内容'
    };

    // 如果包含思维链字段，添加到示例中
    if (includeCOT) {
      example[cotField] = '思维链过程内容';
    }

    if (includeLabels) {
      example.labels = ['领域标签1'];
    }

    if (includeChunk) {
      example.chunk = '文本块';
    }

    return fileFormat === 'json' ? JSON.stringify([example], null, 2) : JSON.stringify(example);
  };

  // CSV 自定义格式化示例
  const getPreviewData = () => {
    if (formatType === 'alpaca') {
      // 根据选择的字段类型生成不同的示例
      if (alpacaFieldType === 'instruction') {
        return {
          headers: ['instruction', 'input', 'output', 'system'],
          rows: [
            {
              instruction: '人类指令（必填）',
              input: '',
              output: '模型回答（必填）',
              system: '系统提示词（选填）'
            },
            {
              instruction: '第二个指令',
              input: '',
              output: '第二个回答',
              system: '系统提示词'
            }
          ]
        };
      } else {
        // input
        return {
          headers: ['instruction', 'input', 'output', 'system'],
          rows: [
            {
              instruction: customInstruction || '固定的指令内容',
              input: '人类问题（必填）',
              output: '模型回答（必填）',
              system: '系统提示词（选填）'
            },
            {
              instruction: customInstruction || '固定的指令内容',
              input: '第二个问题',
              output: '第二个回答',
              system: '系统提示词'
            }
          ]
        };
      }
    } else if (formatType === 'sharegpt') {
      return {
        headers: ['messages'],
        rows: [
          {
            messages: JSON.stringify(
              [
                {
                  messages: [
                    {
                      role: 'system',
                      content: '系统提示词（选填）'
                    },
                    {
                      role: 'user',
                      content: '人类指令' // 映射到 question 字段
                    },
                    {
                      role: 'assistant',
                      content: '模型回答' // 映射到 cot+answer 字段
                    }
                  ]
                }
              ],
              null,
              2
            )
          }
        ]
      };
    } else if (formatType === 'custom') {
      const headers = [customFields.questionField, customFields.answerField];
      if (includeCOT) headers.push(customFields.cotField);
      if (customFields.includeLabels) headers.push('labels');
      if (customFields.includeChunk) headers.push('chunkId');

      const row = {
        [customFields.questionField]: '问题内容',
        [customFields.answerField]: '答案内容'
      };
      if (includeCOT) row[customFields.cotField] = '思维链过程内容';
      if (customFields.includeLabels) row.labels = '领域标签';
      if (customFields.includeChunk) row.chunkId = '文本块';
      return {
        headers,
        rows: [row]
      };
    }
  };

  return (
    <>
      <Box sx={{ mb: 3 }}>
        <Typography variant="subtitle1" fontWeight="bold" gutterBottom>
          {t('export.fileFormat')}
        </Typography>
        <FormControl component="fieldset">
          <RadioGroup
            aria-label="fileFormat"
            name="fileFormat"
            value={fileFormat}
            onChange={handleFileFormatChange}
            row
          >
            <FormControlLabel value="json" control={<Radio />} label="JSON" />
            <FormControlLabel value="jsonl" control={<Radio />} label="JSONL" />
            <FormControlLabel value="csv" control={<Radio />} label="CSV" />
          </RadioGroup>
        </FormControl>
      </Box>

      {/* 数据集风格 */}
      <Box sx={{ mb: 3 }}>
        <Typography variant="subtitle1" fontWeight="bold" gutterBottom>
          {t('export.format')}
        </Typography>
        <FormControl component="fieldset">
          <RadioGroup aria-label="format" name="format" value={formatType} onChange={handleFormatChange} row>
            <FormControlLabel value="alpaca" control={<Radio />} label="Alpaca" />
            <FormControlLabel value="sharegpt" control={<Radio />} label="ShareGPT" />
            <FormControlLabel value="custom" control={<Radio />} label={t('export.customFormat')} />
          </RadioGroup>
        </FormControl>
      </Box>

      {/* Alpaca 格式特有的设置 */}
      {formatType === 'alpaca' && (
        <Box sx={{ mb: 3, pl: 2, borderLeft: `1px solid ${theme.palette.divider}` }}>
          <Typography variant="subtitle2" gutterBottom>
            {t('export.alpacaSettings') || 'Alpaca 格式设置'}
          </Typography>
          <FormControl component="fieldset">
            <Typography variant="body2" color="text.secondary" gutterBottom>
              {t('export.questionFieldType') || '问题字段类型'}
            </Typography>
            <RadioGroup
              aria-label="alpacaFieldType"
              name="alpacaFieldType"
              value={alpacaFieldType}
              onChange={handleAlpacaFieldTypeChange}
              row
            >
              <FormControlLabel
                value="instruction"
                control={<Radio />}
                label={t('export.useInstruction') || '使用 instruction 字段'}
              />
              <FormControlLabel value="input" control={<Radio />} label={t('export.useInput') || '使用 input 字段'} />
            </RadioGroup>

            {alpacaFieldType === 'input' && (
              <TextField
                fullWidth
                size="small"
                label={t('export.customInstruction') || '自定义 instruction 字段内容'}
                value={customInstruction}
                onChange={handleCustomInstructionChange}
                margin="normal"
                placeholder={t('export.instructionPlaceholder') || '请输入固定的指令内容'}
                helperText={
                  t('export.instructionHelperText') || '当使用 input 字段时，可以在这里指定固定的 instruction 内容'
                }
              />
            )}
          </FormControl>
        </Box>
      )}

      {/* 自定义格式选项 */}
      {formatType === 'custom' && (
        <Box sx={{ mb: 3, pl: 2, borderLeft: `1px solid ${theme.palette.divider}` }}>
          <Typography variant="subtitle2" gutterBottom>
            {t('export.customFormatSettings')}
          </Typography>
          <Grid container spacing={2}>
            <Grid item xs={12} sm={6}>
              <TextField
                fullWidth
                size="small"
                label={t('export.questionFieldName')}
                value={customFields.questionField}
                onChange={handleCustomFieldChange('questionField')}
                margin="normal"
              />
            </Grid>
            <Grid item xs={12} sm={6}>
              <TextField
                fullWidth
                size="small"
                label={t('export.answerFieldName')}
                value={customFields.answerField}
                onChange={handleCustomFieldChange('answerField')}
                margin="normal"
              />
            </Grid>
            {/* 添加思维链字段名输入框 */}
            <Grid item xs={12} sm={6}>
              <TextField
                fullWidth
                size="small"
                label={t('export.cotFieldName')}
                value={customFields.cotField}
                onChange={handleCustomFieldChange('cotField')}
                margin="normal"
              />
            </Grid>
          </Grid>
          <FormControlLabel
            control={
              <Checkbox checked={customFields.includeLabels} onChange={handleIncludeLabelsChange} size="small" />
            }
            label={t('export.includeLabels')}
          />
          {/* <FormControlLabel
            control={<Checkbox checked={customFields.includeChunk} onChange={handleIncludeChunkChange} size="small" />}
            label={t('export.includeChunk')}
          /> */}
        </Box>
      )}

      <Box sx={{ mb: 3 }}>
        <Typography variant="subtitle1" fontWeight="bold" gutterBottom>
          {t('export.example')}
        </Typography>

        {fileFormat === 'csv' ? (
          <TableContainer component={Paper} sx={{ mb: 2 }}>
            {(() => {
              const { headers, rows } = getPreviewData();
              const tableKey = `${formatType}-${fileFormat}-${JSON.stringify(customFields)}`;
              return (

                  <TableHead>
                    <TableRow>
                      {headers.map(header => (
                        <TableCell key={header}>{header}</TableCell>
                      ))}
                    </TableRow>
                  </TableHead>
                  <TableBody>
                    {rows.map((row, index) => (
                      <TableRow key={index}>
                        {headers.map(header => (
                          <TableCell key={header}>
                            {Array.isArray(row[header]) ? row[header].join(', ') : row[header] || ''}
                          </TableCell>
                        ))}
                      </TableRow>
                    ))}
                  </TableBody>

              );
            })()}
          </TableContainer>
        ) : (
          <Paper
            variant="outlined"
            sx={{
              p: 2,
              backgroundColor: theme.palette.mode === 'dark' ? theme.palette.grey[900] : theme.palette.grey[100],
              overflowX: 'auto'
            }}
          >

              {formatType === 'custom'
                ? getCustomFormatExample()
                : formatType === 'alpaca'
                  ? fileFormat === 'json'
                    ? JSON.stringify(
                        [
                          {
                            instruction: '人类指令（必填）', // 映射到 question 字段
                            input: '人类输入（选填）',
                            output: '模型回答（必填）', // 映射到 cot+answer 字段
                            system: '系统提示词（选填）'
                          }
                        ],
                        null,
                        2
                      )
                    : '{"instruction": "人类指令（必填）", "input": "人类输入（选填）", "output": "模型回答（必填）", "system": "系统提示词（选填）"}\n{"instruction": "第二个指令", "input": "", "output": "第二个回答", "system": "系统提示词"}'
                  : fileFormat === 'json'
                    ? JSON.stringify(
                        [
                          {
                            messages: [
                              {
                                role: 'system',
                                content: '系统提示词（选填）'
                              },
                              {
                                role: 'user',
                                content: '人类指令' // 映射到 question 字段
                              },
                              {
                                role: 'assistant',
                                content: '模型回答' // 映射到 cot+answer 字段
                              }
                            ]
                          }
                        ],
                        null,
                        2
                      )
                    : '{"messages": [{"role": "system", "content": "系统提示词（选填）"}, {"role": "user", "content": "人类指令"}, {"role": "assistant", "content": "模型回答"}]}\n{"messages": [{"role": "user", "content": "第二个问题"}, {"role": "assistant", "content": "第二个回答"}]}'}

          </Paper>
        )}
      </Box>

      <Box sx={{ mb: 3 }}>
        <Typography variant="subtitle1" fontWeight="bold" gutterBottom>
          {t('export.systemPrompt')}
        </Typography>
        <TextField
          fullWidth
          multiline
          rows={3}
          variant="outlined"
          placeholder={t('export.systemPromptPlaceholder')}
          value={systemPrompt}
          onChange={handleSystemPromptChange}
        />
      </Box>

      <Box sx={{ mb: 2, display: 'flex', flexDirection: 'row', gap: 4 }}>
        <FormControlLabel
          control={<Checkbox checked={confirmedOnly} onChange={handleConfirmedOnlyChange} />}
          label={t('export.onlyConfirmed')}
        />

        <FormControlLabel
          control={<Checkbox checked={includeCOT} onChange={handleIncludeCOTChange} />}
          label={t('export.includeCOT')}
        />
      </Box>

      <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}>
        <Button onClick={handleExport} variant="contained" sx={{ borderRadius: 2 }}>
          {t('export.confirmExport')}
        </Button>
      </Box>
    </>
  );
};

export default LocalExportTab;

import { NextResponse } from 'next/server';
import axios from 'axios';

// 从模型提供商获取模型列表
export async function POST(request) {
  try {
    const { endpoint, providerId, apiKey } = await request.json();

    if (!endpoint) {
      return NextResponse.json({ error: '缺少 endpoint 参数' }, { status: 400 });
    }

    let url = endpoint.replace(/\/$/, ''); // 去除末尾的斜杠
    url += providerId === 'ollama' ? '/tags' : '/models';

    const headers = {};
    if (apiKey) {
      headers.Authorization = `Bearer ${apiKey}`;
    }

    const response = await axios.get(url, { headers });

    // 根据不同提供商格式化返回数据
    let formattedModels = [];
    if (providerId === 'ollama') {
      formattedModels = response.data.models.map(item => ({
        modelId: item.model,
        modelName: item.name,
        providerId
      }));
    } else {
      // 默认处理方式（适用于 OpenAI 等）
      formattedModels = response.data.data.map(item => ({
        modelId: item.id,
        modelName: item.id,
        providerId
      }));
    }

    return NextResponse.json(formattedModels);
  } catch (error) {
    console.error('获取模型列表失败:', String(error));

    // 处理特定错误
    if (error.response) {
      if (error.response.status === 401) {
        return NextResponse.json({ error: 'API Key 无效' }, { status: 401 });
      }
      return NextResponse.json(
        { error: `获取模型列表失败: ${error.response.statusText}` },
        { status: error.response.status }
      );
    }

    return NextResponse.json({ error: `获取模型列表失败: ${error.message}` }, { status: 500 });
  }
}

import { getModelConfigById } from '@/lib/db/model-config';
import { getProject } from '@/lib/db/projects';
import logger from '@/lib/util/logger';

/**
 * Get the active model configuration for a project
 * @param {string} projectId - Optional project ID to get the default model for
 * @returns {Promise<Object|null>} - Active model configuration or null
 */
export async function getActiveModel(projectId = null) {
  try {
    // If projectId is provided, get the default model for that project
    if (projectId) {
      const project = await getProject(projectId);
      if (project && project.defaultModelConfigId) {
        const modelConfig = await getModelConfigById(project.defaultModelConfigId);
        if (modelConfig) {
          logger.info(`Using default model for project ${projectId}: ${modelConfig.modelName}`);
          return modelConfig;
        }
      }
    }

    // If no specific project model found, try to get from localStorage context
    // This is a fallback for when the function is called without context
    logger.warn('No active model found');
    return null;
  } catch (error) {
    logger.error('Failed to get active model:', error);
    return null;
  }
}

/**
 * Get active model by ID
 * @param {string} modelConfigId - Model configuration ID
 * @returns {Promise<Object|null>} - Model configuration or null
 */
export async function getModelById(modelConfigId) {
  try {
    if (!modelConfigId) {
      logger.warn('No model ID provided');
      return null;
    }

    const modelConfig = await getModelConfigById(modelConfigId);
    if (modelConfig) {
      logger.info(`Retrieved model: ${modelConfig.modelName}`);
      return modelConfig;
    }

    logger.warn(`Model not found with ID: ${modelConfigId}`);
    return null;
  } catch (error) {
    logger.error('Failed to get model by ID:', error);
    return null;
  }
}

'use client';

import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  Button,
  TextField,
  Typography,
  Box,
  CircularProgress,
  Alert,
  List,
  ListItem,
  ListItemText,
  Paper,
  IconButton,
  Divider
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import axios from 'axios';
import i18n from '@/lib/i18n';

/**
 * 问题生成对话框组件
 * @param {Object} props
 * @param {boolean} props.open - 对话框是否打开
 * @param {Function} props.onClose - 关闭对话框的回调函数
 * @param {Function} props.onGenerated - 问题生成完成的回调函数
 * @param {string} props.projectId - 项目ID
 * @param {Object} props.tag - 标签对象
 * @param {string} props.tagPath - 标签路径
 * @param {Object} props.model - 选择的模型配置
 */
export default function QuestionGenerationDialog({ open, onClose, onGenerated, projectId, tag, tagPath, model }) {
  const { t } = useTranslation();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [count, setCount] = useState(5);
  const [generatedQuestions, setGeneratedQuestions] = useState([]);

  // 处理生成问题
  const handleGenerateQuestions = async () => {
    try {
      setLoading(true);
      setError('');

      const response = await axios.post(`/api/projects/${projectId}/distill/questions`, {
        tagPath,
        currentTag: tag.label,
        tagId: tag.id,
        count,
        model,
        language: i18n.language
      });

      setGeneratedQuestions(response.data);
    } catch (error) {
      console.error('生成问题失败:', error);
      setError(error.response?.data?.error || t('distill.generateQuestionsError'));
    } finally {
      setLoading(false);
    }
  };

  // 处理生成完成
  const handleGenerateComplete = async () => {
    if (onGenerated) {
      onGenerated(generatedQuestions);
    }
    handleClose();
  };

  // 处理关闭对话框
  const handleClose = () => {
    setGeneratedQuestions([]);
    setError('');
    setCount(5);
    if (onClose) {
      onClose();
    }
  };

  // 处理数量变化
  const handleCountChange = event => {
    const value = parseInt(event.target.value);
    if (!isNaN(value) && value >= 1 && value <= 100) {
      setCount(value);
    }
  };

  return (
    <Dialog
      open={open}
      onClose={handleClose}
      maxWidth="sm"
      fullWidth
      PaperProps={{
        sx: { borderRadius: 2 }
      }}
    >
      <DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <Typography variant="h6" component="div">
          {t('distill.generateQuestionsTitle', { tag: tag?.label || t('distill.unknownTag') })}
        </Typography>
        <IconButton edge="end" color="inherit" onClick={handleClose} aria-label="close">
          <CloseIcon />
        </IconButton>
      </DialogTitle>

      <DialogContent dividers>
        {error && (
          <Alert severity="error" sx={{ mb: 2 }}>
            {error}
          </Alert>
        )}

        <Box sx={{ mb: 3 }}>
          <Typography variant="subtitle1" gutterBottom>
            {t('distill.tagPath')}:
          </Typography>
          <Paper variant="outlined" sx={{ p: 2, borderRadius: 1, backgroundColor: 'background.paper' }}>
            <Typography variant="body1">{tagPath || tag?.label || t('distill.unknownTag')}</Typography>
          </Paper>
        </Box>

        <Box sx={{ mb: 3 }}>
          <Typography variant="subtitle1" gutterBottom>
            {t('distill.questionCount')}:
          </Typography>
          <TextField
            fullWidth
            variant="outlined"
            type="number"
            value={count}
            onChange={handleCountChange}
            inputProps={{ min: 1, max: 100 }}
            disabled={loading}
            helperText={t('distill.questionCountHelp')}
          />
        </Box>

        {generatedQuestions.length > 0 && (
          <Box>
            <Typography variant="subtitle1" gutterBottom>
              {t('distill.generatedQuestions')}:
            </Typography>
            <Paper variant="outlined" sx={{ p: 0, borderRadius: 1, backgroundColor: 'background.paper' }}>
              <List disablePadding>
                {generatedQuestions.map((question, index) => (
                  <React.Fragment key={index}>
                    {index > 0 && <Divider />}
                    <ListItem>
                      <ListItemText
                        primary={question.question}
                        primaryTypographyProps={{
                          style: { whiteSpace: 'normal', wordBreak: 'break-word' }
                        }}
                      />
                    </ListItem>
                  </React.Fragment>
                ))}
              </List>
            </Paper>
          </Box>
        )}
      </DialogContent>

      <DialogActions sx={{ p: 2 }}>
        <Button onClick={handleClose} color="inherit">
          {t('common.cancel')}
        </Button>
        {generatedQuestions.length > 0 ? (
          <Button onClick={handleGenerateComplete} color="primary" variant="contained">
            {t('common.complete')}
          </Button>
        ) : (
          <Button
            variant="contained"
            color="primary"
            onClick={handleGenerateQuestions}
            disabled={loading}
            startIcon={loading && <CircularProgress size={20} color="inherit" />}
          >
            {loading ? t('common.generating') : t('distill.generateQuestions')}
          </Button>
        )}
      </DialogActions>
    </Dialog>
  );
}

'use client';

import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  Button,
  TextField,
  Typography,
  Box,
  CircularProgress,
  Alert,
  Chip,
  Paper,
  IconButton
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import axios from 'axios';
import i18n from '@/lib/i18n';

/**
 * 标签生成对话框组件
 * @param {Object} props
 * @param {boolean} props.open - 对话框是否打开
 * @param {Function} props.onClose - 关闭对话框的回调函数
 * @param {Function} props.onGenerated - 标签生成完成的回调函数
 * @param {string} props.projectId - 项目ID
 * @param {Object} props.parentTag - 父标签对象，为null时表示生成根标签
 * @param {string} props.tagPath - 标签链路
 * @param {Object} props.model - 选择的模型配置
 */
export default function TagGenerationDialog({ open, onClose, onGenerated, projectId, parentTag, tagPath, model }) {
  const { t } = useTranslation();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [count, setCount] = useState(5);
  const [generatedTags, setGeneratedTags] = useState([]);
  const [parentTagName, setParentTagName] = useState('');
  const [project, setProject] = useState(null);

  // 获取项目信息，如果是顶级标签，默认填写项目名称
  useEffect(() => {
    if (projectId && !parentTag) {
      axios
        .get(`/api/projects/${projectId}`)
        .then(response => {
          setProject(response.data);
          setParentTagName(response.data.name || '');
        })
        .catch(error => {
          console.error('获取项目信息失败:', error);
        });
    } else if (parentTag) {
      setParentTagName(parentTag.label || '');
    }
  }, [projectId, parentTag]);

  // 处理生成标签
  const handleGenerateTags = async () => {
    try {
      setLoading(true);
      setError('');

      const response = await axios.post(`/api/projects/${projectId}/distill/tags`, {
        parentTag: parentTagName,
        parentTagId: parentTag ? parentTag.id : null,
        tagPath: tagPath || parentTagName,
        count,
        model,
        language: i18n.language
      });

      setGeneratedTags(response.data);
    } catch (error) {
      console.error('生成标签失败:', error);
      setError(error.response?.data?.error || t('distill.generateTagsError'));
    } finally {
      setLoading(false);
    }
  };

  // 处理生成完成
  const handleGenerateComplete = async () => {
    if (onGenerated) {
      onGenerated(generatedTags);
    }
    handleClose();
  };

  // 处理关闭对话框
  const handleClose = () => {
    setGeneratedTags([]);
    setError('');
    setCount(5);
    if (onClose) {
      onClose();
    }
  };

  // 处理数量变化
  const handleCountChange = event => {
    const value = parseInt(event.target.value);
    if (!isNaN(value) && value >= 1 && value <= 100) {
      setCount(value);
    }
  };

  return (
    <Dialog
      open={open}
      onClose={handleClose}
      maxWidth="sm"
      fullWidth
      PaperProps={{
        sx: { borderRadius: 2 }
      }}
    >
      <DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <Typography variant="h6" component="div">
          {parentTag
            ? t('distill.generateSubTagsTitle', { parentTag: parentTag.label })
            : t('distill.generateRootTagsTitle')}
        </Typography>
        <IconButton edge="end" color="inherit" onClick={handleClose} aria-label="close">
          <CloseIcon />
        </IconButton>
      </DialogTitle>

      <DialogContent dividers>
        {error && (
          <Alert severity="error" sx={{ mb: 2 }}>
            {error}
          </Alert>
        )}

        {/* 标签路径显示 */}
        {parentTag && tagPath && (
          <Box sx={{ mb: 3 }}>
            <Typography variant="subtitle1" gutterBottom>
              {t('distill.tagPath')}:
            </Typography>
            <Paper variant="outlined" sx={{ p: 2, borderRadius: 1, backgroundColor: 'background.paper' }}>
              <Typography variant="body1">{tagPath || parentTag.label}</Typography>
            </Paper>
          </Box>
        )}

        <Box sx={{ mb: 3 }}>
          <Typography variant="subtitle1" gutterBottom>
            {t('distill.parentTag')}:
          </Typography>

          <TextField
            fullWidth
            variant="outlined"
            value={parentTagName}
            onChange={e => setParentTagName(e.target.value)}
            placeholder={t('distill.parentTagPlaceholder')}
            disabled={loading || !parentTag}
            // 如果是顶级标签，设置为只读
            InputProps={{
              readOnly: !parentTag
            }}
            // 显示适当的帮助文本
            helperText={
              !parentTag
                ? t('distill.rootTopicHelperText', { defaultValue: '使用项目名称作为顶级主题' })
                : t('distill.parentTagHelp')
            }
          />
        </Box>

        <Box sx={{ mb: 3 }}>
          <Typography variant="subtitle1" gutterBottom>
            {t('distill.tagCount')}:
          </Typography>
          <TextField
            fullWidth
            variant="outlined"
            type="number"
            value={count}
            onChange={handleCountChange}
            inputProps={{ min: 1, max: 100 }}
            disabled={loading}
            helperText={t('distill.tagCountHelp')}
          />
        </Box>

        {generatedTags.length > 0 && (
          <Box>
            <Typography variant="subtitle1" gutterBottom>
              {t('distill.generatedTags')}:
            </Typography>
            <Paper variant="outlined" sx={{ p: 2, borderRadius: 1, backgroundColor: 'background.paper' }}>
              <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
                {generatedTags.map((tag, index) => (
                  <Chip key={index} label={tag.label} color="primary" variant="outlined" />
                ))}
              </Box>
            </Paper>
          </Box>
        )}
      </DialogContent>

      <DialogActions sx={{ p: 2 }}>
        <Button onClick={handleClose} color="inherit">
          {t('common.cancel')}
        </Button>
        {generatedTags.length > 0 ? (
          <Button onClick={handleGenerateComplete} color="primary" variant="contained">
            {t('common.complete')}
          </Button>
        ) : (
          <Button
            variant="contained"
            color="primary"
            onClick={handleGenerateTags}
            disabled={loading || !parentTagName}
            startIcon={loading && <CircularProgress size={20} color="inherit" />}
          >
            {loading ? t('common.generating') : t('distill.generateTags')}
          </Button>
        )}
      </DialogActions>
    </Dialog>
  );
}

/**
 * Markdown目录提取模块
 */

/**
 * 提取Markdown文档的目录结构
 * @param {string} text - Markdown文本
 * @param {Object} options - 配置选项
 * @param {number} options.maxLevel - 提取的最大标题级别，默认为6
 * @param {boolean} options.includeLinks - 是否包含锚点链接，默认为true
 * @param {boolean} options.flatList - 是否返回扁平列表，默认为false（返回嵌套结构）
 * @returns {Array} - 目录结构数组
 */
function extractTableOfContents(text, options = {}) {
  const { maxLevel = 6, includeLinks = true, flatList = false } = options;

  // 匹配标题的正则表达式
  const headingRegex = /^(#{1,6})\s+(.+?)(?:\s*\{#[\w-]+\})?\s*$/gm;
  const tocItems = [];
  let match;

  while ((match = headingRegex.exec(text)) !== null) {
    const level = match[1].length;

    // 如果标题级别超过了设定的最大级别，则跳过
    if (level > maxLevel) {
      continue;
    }

    const title = match[2].trim();
    const position = match.index;

    // 生成锚点ID（用于链接）
    const anchorId = generateAnchorId(title);

    tocItems.push({
      level,
      title,
      position,
      anchorId,
      children: []
    });
  }

  // 如果需要返回扁平列表，直接返回处理后的结果
  if (flatList) {
    return tocItems.map(item => {
      const result = {
        level: item.level,
        title: item.title,
        position: item.position
      };

      if (includeLinks) {
        result.link = `#${item.anchorId}`;
      }

      return result;
    });
  }

  // 构建嵌套结构
  return buildNestedToc(tocItems, includeLinks);
}

/**
 * 生成标题的锚点ID
 * @param {string} title - 标题文本
 * @returns {string} - 生成的锚点ID
 */
function generateAnchorId(title) {
  return title
    .toLowerCase()
    .replace(/\s+/g, '-')
    .replace(/[^\w\-]/g, '')
    .replace(/\-+/g, '-')
    .replace(/^\-+|\-+$/g, '');
}

/**
 * 构建嵌套的目录结构
 * @param {Array} items - 扁平的目录项数组
 * @param {boolean} includeLinks - 是否包含链接
 * @returns {Array} - 嵌套的目录结构
 */
function buildNestedToc(items, includeLinks) {
  const result = [];
  const stack = [{ level: 0, children: result }];

  items.forEach(item => {
    const tocItem = {
      title: item.title,
      level: item.level,
      position: item.position,
      children: []
    };

    if (includeLinks) {
      tocItem.link = `#${item.anchorId}`;
    }

    // 找到当前项的父级
    while (stack[stack.length - 1].level >= item.level) {
      stack.pop();
    }

    // 将当前项添加到父级的children中
    stack[stack.length - 1].children.push(tocItem);

    // 将当前项入栈
    stack.push(tocItem);
  });

  return result;
}

/**
 * 将目录结构转换为Markdown格式
 * @param {Array} toc - 目录结构（嵌套或扁平）
 * @param {Object} options - 配置选项
 * @param {boolean} options.isNested - 是否为嵌套结构，默认为true
 * @param {boolean} options.includeLinks - 是否包含链接，默认为true
 * @returns {string} - Markdown格式的目录
 */
function tocToMarkdown(toc, options = {}) {
  const { isNested = true, includeLinks = true } = options;

  if (isNested) {
    return nestedTocToMarkdown(toc, 0, includeLinks);
  } else {
    return flatTocToMarkdown(toc, includeLinks);
  }
}

/**
 * 将嵌套的目录结构转换为Markdown格式
 * @private
 */
function nestedTocToMarkdown(items, indent = 0, includeLinks) {
  let result = '';
  const indentStr = '  '.repeat(indent);

  // 添加数据验证
  if (!Array.isArray(items)) {
    console.warn('Warning: items is not an array in nestedTocToMarkdown');
    return result;
  }

  items.forEach(item => {
    const titleText = includeLinks && item.link ? `[${item.title}](${item.link})` : item.title;

    result += `${indentStr}- ${titleText}\n`;

    if (item.children && item.children.length > 0) {
      result += nestedTocToMarkdown(item.children, indent + 1, includeLinks);
    }
  });

  return result;
}

/**
 * 将扁平的目录结构转换为Markdown格式
 * @private
 */
function flatTocToMarkdown(items, includeLinks) {
  let result = '';

  items.forEach(item => {
    const indent = '  '.repeat(item.level - 1);
    const titleText = includeLinks && item.link ? `[${item.title}](${item.link})` : item.title;

    result += `${indent}- ${titleText}\n`;
  });

  return result;
}

module.exports = {
  extractTableOfContents,
  tocToMarkdown
};

/**
 * 文件操作辅助函数
 */
const path = require('path');
const { promises: fs } = require('fs');
const { getProjectRoot, readJSON } = require('./base');
const { getProject } = require('./projects');
const { getUploadFileInfoById } = require('./upload-files');

/**
 * 获取项目文件内容
 * @param {string} projectId - 项目ID
 * @param {string} fileName - 文件名
 * @returns {Promise<string>} 文件内容
 */
async function getProjectFileContent(projectId, fileName) {
  try {
    // 获取项目根目录
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);
    const filePath = path.join(projectPath, 'files', fileName);

    // 读取文件内容
    const content = await fs.readFile(filePath, 'utf-8');
    return content;
  } catch (error) {
    console.error('获取项目文件内容失败:', error);
    return '';
  }
}

/**
 * 根据文件ID获取项目文件内容
 * @param {string} projectId - 项目ID
 * @param {string} fileId - 文件ID
 * @returns {Promise<string>} 文件内容
 */
async function getProjectFileContentById(projectId, fileId) {
  try {
    // 获取文件信息
    const fileInfo = await getUploadFileInfoById(fileId);
    if (!fileInfo) {
      throw new Error('文件不存在');
    }

    // 获取项目根目录
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);
    const filePath = path.join(projectPath, 'files', fileInfo.fileName);

    // 读取文件内容
    const content = await fs.readFile(filePath, 'utf-8');
    return content;
  } catch (error) {
    console.error('根据ID获取项目文件内容失败:', error);
    return '';
  }
}

module.exports = {
  getProjectFileContent,
  getProjectFileContentById
};

'use client';

import React, { useState } from 'react';
import {
  AppBar,
  Toolbar,
  Typography,
  Box,
  MenuItem,
  FormControl,
  Select,
  Tabs,
  Tab,
  IconButton,
  useTheme as useMuiTheme,
  Tooltip,
  Menu,
  ListItemIcon,
  ListItemText,
  Divider
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import ModelSelect from './ModelSelect';
import LanguageSwitcher from './LanguageSwitcher';
import UpdateChecker from './UpdateChecker';
import TaskIcon from './TaskIcon';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useTheme } from 'next-themes';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';

// 图标
import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';
import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined';
import StorageIcon from '@mui/icons-material/Storage';
import GitHubIcon from '@mui/icons-material/GitHub';
import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';
import TokenOutlinedIcon from '@mui/icons-material/TokenOutlined';
import QuestionAnswerOutlinedIcon from '@mui/icons-material/QuestionAnswerOutlined';
import DatasetOutlinedIcon from '@mui/icons-material/DatasetOutlined';
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
import ScienceOutlinedIcon from '@mui/icons-material/ScienceOutlined';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { toast } from 'sonner';
import axios from 'axios';
import { useSetAtom } from 'jotai/index';
import { modelConfigListAtom, selectedModelInfoAtom } from '@/lib/store';

export default function Navbar({ projects = [], currentProject }) {
  const [selectedProject, setSelectedProject] = useState(currentProject || '');
  const { t, i18n } = useTranslation();
  const pathname = usePathname();
  const theme = useMuiTheme();
  const { resolvedTheme, setTheme } = useTheme();
  const setConfigList = useSetAtom(modelConfigListAtom);
  const setSelectedModelInfo = useSetAtom(selectedModelInfoAtom);
  // 只在项目详情页显示模块选项卡
  const isProjectDetail = pathname.includes('/projects/') && pathname.split('/').length > 3;
  // 更多菜单状态
  const [moreMenuAnchor, setMoreMenuAnchor] = useState(null);
  const isMoreMenuOpen = Boolean(moreMenuAnchor);

  // 处理更多菜单打开
  const handleMoreMenuOpen = event => {
    setMoreMenuAnchor(event.currentTarget);
  };

  // 处理更多菜单悬浮打开
  const handleMoreMenuHover = event => {
    setMoreMenuAnchor(event.currentTarget);
  };

  // 关闭更多菜单
  const handleMoreMenuClose = () => {
    setMoreMenuAnchor(null);
  };

  // 处理菜单区域的鼠标离开
  const handleMenuMouseLeave = () => {
    setMoreMenuAnchor(null);
  };

  const handleProjectChange = event => {
    const newProjectId = event.target.value;
    setSelectedProject(newProjectId);
    axios
      .get(`/api/projects/${newProjectId}/model-config`)
      .then(response => {
        setConfigList(response.data.data);
        if (response.data.defaultModelConfigId) {
          setSelectedModelInfo(response.data.data.find(item => item.id === response.data.defaultModelConfigId));
        } else {
          setSelectedModelInfo('');
        }
      })
      .catch(error => {
        toast.error('get model list error');
      });
    // 跳转到新选择的项目页面
    window.location.href = `/projects/${newProjectId}/text-split`;
  };

  const toggleTheme = () => {
    setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
  };

  return (
    <AppBar
      position="static"
      elevation={0}
      color={theme.palette.mode === 'dark' ? 'transparent' : 'primary'}
      sx={{
        borderBottom: `1px solid ${theme.palette.divider}`,
        bgcolor: theme.palette.mode === 'dark' ? 'background.paper' : 'primary.main'
      }}
      style={{ borderRadius: 0, zIndex: 99000 }}
    >
      <Toolbar
        sx={{
          height: '56px',
          minHeight: '56px',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between'
        }}
        style={{ zIndex: 99000 }}
      >
        {/* 左侧Logo和项目选择 */}
        <Box sx={{ display: 'flex', alignItems: 'center', flexGrow: 0 }}>
          <Box
            sx={{
              display: 'flex',
              alignItems: 'center',
              mr: 2,
              '&:hover': { opacity: 0.9 }
            }}
            style={{ cursor: 'pointer', '&:hover': { opacity: 0.9 } }}
            onClick={() => {
              window.location.href = '/';
            }}
          >
            <Box
              component="img"
              src="/imgs/logo.svg"
              alt="Easy Dataset Logo"
              sx={{
                width: 28,
                height: 28,
                mr: 1.5
              }}
            />
            <Typography
              variant="h6"
              component="div"
              sx={{
                fontWeight: 600,
                letterSpacing: '-0.5px'
              }}
              style={{ fontSize: '1.1rem' }}
              className={theme.palette.mode === 'dark' ? 'gradient-text' : ''}
              color={theme.palette.mode === 'dark' ? 'inherit' : 'white'}
            >
              Easy DataSet
            </Typography>
          </Box>

          {isProjectDetail && (
            <FormControl size="small" sx={{ minWidth: 100 }}>
              <Select
                value={selectedProject}
                onChange={handleProjectChange}
                displayEmpty
                variant="outlined"
                sx={{
                  bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(255, 255, 255, 0.15)',
                  borderRadius: '8px',
                  color: theme.palette.mode === 'dark' ? 'inherit' : 'white',
                  '& .MuiSelect-icon': {
                    color: theme.palette.mode === 'dark' ? 'inherit' : 'white'
                  },
                  '& .MuiOutlinedInput-notchedOutline': {
                    borderColor: 'transparent'
                  },
                  '&:hover .MuiOutlinedInput-notchedOutline': {
                    borderColor: 'transparent'
                  },
                  '&.Mui-focused .MuiOutlinedInput-notchedOutline': {
                    borderColor: 'primary.main'
                  }
                }}
                MenuProps={{
                  PaperProps: {
                    elevation: 2,
                    sx: { mt: 1, borderRadius: 2 }
                  }
                }}
              >
                <MenuItem value="" disabled>
                  {t('projects.selectProject')}
                </MenuItem>
                {projects.map(project => (
                  <MenuItem key={project.id} value={project.id}>
                    {project.name}
                  </MenuItem>
                ))}
              </Select>
            </FormControl>
          )}
        </Box>

        {/* 中间的功能模块导航 - 使用Flex布局居中 */}
        {isProjectDetail && (
          <Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'center', ml: 2, mr: 2 }}>
            <Tabs
              value={
                pathname.includes('/settings') || pathname.includes('/playground') || pathname.includes('/datasets-sq')
                  ? 'more'
                  : pathname
              }
              textColor="inherit"
              indicatorColor="secondary"
              sx={{
                '& .MuiTab-root': {
                  minWidth: 100,
                  fontSize: '0.85rem',
                  transition: 'all 0.2s',
                  color: theme.palette.mode === 'dark' ? 'inherit' : 'white',
                  opacity: theme.palette.mode === 'dark' ? 0.7 : 0.8,
                  padding: '6px 16px',
                  minHeight: '48px',
                  '&:hover': {
                    color: theme.palette.mode === 'dark' ? theme.palette.secondary.main : 'white',
                    opacity: 1
                  }
                },
                '& .Mui-selected': {
                  color: theme.palette.mode === 'dark' ? theme.palette.secondary.main : 'white',
                  opacity: 1,
                  fontWeight: 600
                },
                '& .MuiTabs-indicator': {
                  backgroundColor: theme.palette.mode === 'dark' ? theme.palette.secondary.main : 'white'
                }
              }}
            >
              <Tab
                icon={
                  <Box sx={{ mr: 1, display: 'flex', alignItems: 'center' }}>
                    <DescriptionOutlinedIcon fontSize="small" />
                  </Box>
                }
                iconPosition="start"
                label={t('textSplit.title')}
                value={`/projects/${selectedProject}/text-split`}
                component={Link}
                href={`/projects/${selectedProject}/text-split`}
              />
              <Tab
                icon={
                  <Box sx={{ mr: 1, display: 'flex', alignItems: 'center' }}>
                    <TokenOutlinedIcon fontSize="small" />
                  </Box>
                }
                iconPosition="start"
                label={t('distill.title')}
                value={`/projects/${selectedProject}/distill`}
                component={Link}
                href={`/projects/${selectedProject}/distill`}
              />
              <Tab
                icon={
                  <Box sx={{ mr: 1, display: 'flex', alignItems: 'center' }}>
                    <QuestionAnswerOutlinedIcon fontSize="small" />
                  </Box>
                }
                iconPosition="start"
                label={t('questions.title')}
                value={`/projects/${selectedProject}/questions`}
                component={Link}
                href={`/projects/${selectedProject}/questions`}
              />
              <Tab
                icon={
                  <Box sx={{ mr: 1, display: 'flex', alignItems: 'center' }}>
                    <DatasetOutlinedIcon fontSize="small" />
                  </Box>
                }
                iconPosition="start"
                label={t('datasets.management')}
                value={`/projects/${selectedProject}/datasets`}
                component={Link}
                href={`/projects/${selectedProject}/datasets`}
              />
              <Tab
                icon={
                  <Box sx={{ mr: 1, display: 'flex', alignItems: 'center' }}>
                    <MoreVertIcon fontSize="small" />
                  </Box>
                }
                iconPosition="start"
                label={t('common.more')}
                onMouseEnter={handleMoreMenuHover}
                value="more"
              />
            </Tabs>
          </Box>
        )}

        {/* 更多菜单 */}
        <Menu
          anchorEl={moreMenuAnchor}
          open={isMoreMenuOpen}
          onClose={handleMoreMenuClose}
          PaperProps={{
            elevation: 2,
            sx: { mt: 1.5, borderRadius: 2, minWidth: 180 },
            onMouseLeave: handleMenuMouseLeave
          }}
          transformOrigin={{ horizontal: 'center', vertical: 'top' }}
          anchorOrigin={{ horizontal: 'center', vertical: 'bottom' }}
          MenuListProps={{
            dense: true,
            onMouseLeave: handleMenuMouseLeave
          }}
        >
          <MenuItem
            component={Link}
            href={`/projects/${selectedProject}/settings`}
            onClick={handleMoreMenuClose}
            selected={pathname === `/projects/${selectedProject}/settings`}
          >
            <ListItemIcon>
              <SettingsOutlinedIcon fontSize="small" />
            </ListItemIcon>
            <ListItemText primary={t('settings.title')} />
          </MenuItem>
          <Divider />
          <MenuItem
            component={Link}
            href={`/projects/${selectedProject}/playground`}
            onClick={handleMoreMenuClose}
            selected={pathname === `/projects/${selectedProject}/playground`}
          >
            <ListItemIcon>
              <ScienceOutlinedIcon fontSize="small" />
            </ListItemIcon>
            <ListItemText primary={t('playground.title')} />
          </MenuItem>
          <Divider />
          <MenuItem
            component={Link}
            href="/dataset-square"
            onClick={handleMoreMenuClose}
            selected={pathname === `/dataset-square`}
          >
            <ListItemIcon>
              <StorageIcon fontSize="small" />
            </ListItemIcon>
            <ListItemText primary={t('datasetSquare.title')} />
          </MenuItem>
        </Menu>

        {/* 右侧操作区 - 使用Flex布局 */}
        <Box sx={{ display: 'flex', flexGrow: 0, alignItems: 'center', gap: 1.5 }}>
          {/* 模型选择 */}
          {location.pathname.includes('/projects/') && <ModelSelect projectId={selectedProject} />}
          {/* 任务图标 - 仅在项目页面显示 */}
          {location.pathname.includes('/projects/') && <TaskIcon theme={theme} projectId={selectedProject} />}

          {/* 语言切换器 */}
          <Box sx={{ display: 'flex', alignItems: 'center' }}>
            <LanguageSwitcher />
          </Box>
          {/* 主题切换按钮 */}
          <Tooltip title={resolvedTheme === 'dark' ? t('theme.switchToLight') : t('theme.switchToDark')}>
            <IconButton
              onClick={toggleTheme}
              size="small"
              sx={{
                bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(255, 255, 255, 0.15)',
                color: theme.palette.mode === 'dark' ? 'inherit' : 'white',
                p: 1,
                borderRadius: 1.5,
                '&:hover': {
                  bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.25)'
                }
              }}
            >
              {resolvedTheme === 'dark' ? (
                <LightModeOutlinedIcon fontSize="small" />
              ) : (
                <DarkModeOutlinedIcon fontSize="small" />
              )}
            </IconButton>
          </Tooltip>

          {/* 文档链接 */}
          <Tooltip title={t('documentation')}>
            <IconButton
              href={
                i18n.language === 'zh-CN' ? 'https://docs.easy-dataset.com/' : 'https://docs.easy-dataset.com/ed/en'
              }
              target="_blank"
              rel="noopener noreferrer"
              size="small"
              sx={{
                bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(255, 255, 255, 0.15)',
                color: theme.palette.mode === 'dark' ? 'inherit' : 'white',
                p: 1,
                borderRadius: 1.5,
                '&:hover': {
                  bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.25)'
                },
                mr: 1,
                marginRight: 0
              }}
            >
              <HelpOutlineIcon fontSize="small" />
            </IconButton>
          </Tooltip>

          {/* GitHub链接 */}
          <Tooltip title={t('common.visitGitHub')}>
            <IconButton
              onClick={() => window.open('https://github.com/ConardLi/easy-dataset', '_blank')}
              size="small"
              sx={{
                bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(255, 255, 255, 0.15)',
                color: theme.palette.mode === 'dark' ? 'inherit' : 'white',
                p: 1,
                borderRadius: 1.5,
                '&:hover': {
                  bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.25)'
                }
              }}
            >
              <GitHubIcon fontSize="small" />
            </IconButton>
          </Tooltip>

          {/* 更新检查器 */}
          <UpdateChecker />
        </Box>
      </Toolbar>
    </AppBar>
  );
}

'use client';

import { useState, useEffect } from 'react';
import {
  Box,
  Typography,
  IconButton,
  Chip,
  Checkbox,
  Tooltip,
  Card,
  CardContent,
  CardActions,
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  Button,
  TextField
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import VisibilityIcon from '@mui/icons-material/Visibility';
import QuizIcon from '@mui/icons-material/Quiz';
import EditIcon from '@mui/icons-material/Edit';
import { useTheme } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';

// 编辑文本块对话框组件
const EditChunkDialog = ({ open, chunk, onClose, onSave }) => {
  const [content, setContent] = useState(chunk?.content || '');
  const { t } = useTranslation();

  // 当文本块变化时更新内容
  useEffect(() => {
    if (chunk?.content) {
      setContent(chunk.content);
    }
  }, [chunk]);

  const handleSave = () => {
    onSave(content);
    onClose();
  };

  return (
    <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
      <DialogTitle>{t('textSplit.editChunk', { chunkId: chunk?.name })}</DialogTitle>
      <DialogContent dividers>
        <TextField
          fullWidth
          multiline
          rows={15}
          value={content}
          onChange={e => setContent(e.target.value)}
          variant="outlined"
          sx={{ mt: 1 }}
        />
      </DialogContent>
      <DialogActions>
        <Button onClick={onClose}>{t('common.cancel')}</Button>
        <Button onClick={handleSave} variant="contained" color="primary">
          {t('common.save')}
        </Button>
      </DialogActions>
    </Dialog>
  );
};

export default function ChunkCard({
  chunk,
  selected,
  onSelect,
  onView,
  onDelete,
  onGenerateQuestions,
  onEdit,
  projectId,
  selectedModel // 添加selectedModel参数
}) {
  const theme = useTheme();
  const { t } = useTranslation();
  const [editDialogOpen, setEditDialogOpen] = useState(false);
  const [chunkForEdit, setChunkForEdit] = useState(null);

  // 获取文本预览
  const getTextPreview = (content, maxLength = 150) => {
    if (!content) return '';
    return content.length > maxLength ? `${content.substring(0, maxLength)}...` : content;
  };

  // 检查是否有已生成的问题
  const hasQuestions = chunk.questions && chunk.questions.length > 0;

  // 处理编辑按钮点击
  const handleEditClick = async () => {
    try {
      // 显示加载状态
      console.log('正在获取文本块完整内容...');
      console.log('projectId:', projectId, 'chunkId:', chunk.id);

      // 先获取完整的文本块内容，使用从外部传入的 projectId
      const response = await fetch(`/api/projects/${projectId}/chunks/${encodeURIComponent(chunk.id)}`);

      if (!response.ok) {
        throw new Error(t('textSplit.fetchChunkFailed'));
      }

      const data = await response.json();
      console.log('获取文本块完整内容成功:', data);

      // 先设置完整数据，再打开对话框（与 ChunkList.js 中的实现一致）
      setChunkForEdit(data);
      setEditDialogOpen(true);
    } catch (error) {
      console.error(t('textSplit.fetchChunkError'), error);
      // 如果出错，使用原始预览数据
      alert(t('textSplit.fetchChunkError'));
    }
  };

  // 处理保存编辑内容
  const handleSaveEdit = newContent => {
    if (onEdit) {
      onEdit(chunk.id, newContent);
    }
  };

  return (
    <>
      <Card
        variant="outlined"
        sx={{
          mb: 1,
          position: 'relative',
          transition: 'all 0.2s ease-in-out',
          borderColor: selected ? theme.palette.primary.main : theme.palette.divider,
          bgcolor: selected ? `${theme.palette.primary.main}10` : 'transparent',
          borderRadius: 2,
          '&:hover': {
            borderColor: theme.palette.primary.main,
            transform: 'translateY(-2px)',
            boxShadow: `0 4px 12px ${theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.1)'}`
          }
        }}
      >
        <CardContent sx={{ pt: 2.5, px: 2.5, pb: '16px !important' }}>
          <Box sx={{ display: 'flex', alignItems: 'flex-start' }}>
            <Checkbox
              checked={selected}
              onChange={onSelect}
              sx={{
                mr: 1,
                '&.Mui-checked': {
                  color: theme.palette.primary.main
                }
              }}
            />
            <Box sx={{ flexGrow: 1 }}>
              <Box
                sx={{
                  display: 'flex',
                  justifyContent: 'space-between',
                  alignItems: 'center',
                  mb: 1.5,
                  flexWrap: 'wrap',
                  gap: 1
                }}
              >
                <Typography
                  variant="subtitle1"
                  fontWeight="600"
                  sx={{
                    color: theme.palette.mode === 'dark' ? theme.palette.primary.light : theme.palette.primary.dark
                  }}
                >
                  {chunk.name}
                </Typography>
                <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
                  <Chip
                    label={`${chunk.fileName || t('textSplit.unknownFile')}`}
                    size="small"
                    color="primary"
                    variant="outlined"
                    sx={{
                      borderRadius: 1,
                      fontWeight: 500,
                      '& .MuiChip-label': { px: 1 }
                    }}
                  />
                  <Chip
                    label={`${chunk.size || 0} ${t('textSplit.characters')}`}
                    size="small"
                    color="secondary"
                    variant="outlined"
                    sx={{
                      borderRadius: 1,
                      fontWeight: 500,
                      '& .MuiChip-label': { px: 1 }
                    }}
                  />
                  {chunk.Questions.length > 0 && (
                    <Tooltip
                      title={
                        <Box sx={{ p: 1 }} style={{ maxHeight: '200px', overflow: 'auto' }}>
                          {chunk.Questions.map((q, index) => (
                            <Typography key={index} variant="body2" sx={{ mb: 0.5 }}>
                              {index + 1}. {q.question}
                            </Typography>
                          ))}
                        </Box>
                      }
                      arrow
                      placement="top"
                    >
                      <Chip
                        label={`${t('textSplit.generatedQuestions', { count: chunk.Questions.length })}`}
                        size="small"
                        color="success"
                        variant="outlined"
                        sx={{
                          borderRadius: 1,
                          fontWeight: 500,
                          '& .MuiChip-label': { px: 1 }
                        }}
                      />
                    </Tooltip>
                  )}
                </Box>
              </Box>

              <Typography
                variant="body2"
                color="textSecondary"
                sx={{
                  mb: 2,
                  lineHeight: 1.6,
                  opacity: 0.85
                }}
              >
                {getTextPreview(chunk.content)}
              </Typography>
            </Box>
          </Box>
        </CardContent>

        <CardActions
          sx={{
            justifyContent: 'flex-end',
            px: 2.5,
            pb: 2,
            gap: 1,
            '& .MuiIconButton-root': {
              transition: 'all 0.2s',
              '&:hover': {
                transform: 'scale(1.1)'
              }
            }
          }}
        >
          <Tooltip title={t('textSplit.viewDetails')}>
            <IconButton
              size="small"
              color="primary"
              onClick={onView}
              sx={{
                bgcolor: theme.palette.mode === 'dark' ? 'rgba(144, 202, 249, 0.08)' : 'rgba(33, 150, 243, 0.08)'
              }}
            >
              <VisibilityIcon fontSize="small" />
            </IconButton>
          </Tooltip>

          <Tooltip
            title={
              selectedModel?.id
                ? t('textSplit.generateQuestions')
                : t('textSplit.selectModelFirst', { defaultValue: '请先在右上角选择模型' })
            }
          >
            <span>
              <IconButton
                size="small"
                color="info"
                onClick={onGenerateQuestions}
                disabled={!selectedModel?.id}
                sx={{
                  bgcolor: theme.palette.mode === 'dark' ? 'rgba(41, 182, 246, 0.08)' : 'rgba(2, 136, 209, 0.08)',
                  '&.Mui-disabled': {
                    opacity: 0.6,
                    pointerEvents: 'auto' // 允许鼠标悬停显示tooltip
                  }
                }}
              >
                <QuizIcon fontSize="small" />
              </IconButton>
            </span>
          </Tooltip>

          <Tooltip title={t('textSplit.editChunk', { chunkId: chunk.name })}>
            <IconButton
              size="small"
              color="warning"
              onClick={handleEditClick}
              sx={{
                bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 152, 0, 0.08)' : 'rgba(237, 108, 2, 0.08)'
              }}
            >
              <EditIcon fontSize="small" />
            </IconButton>
          </Tooltip>

          <Tooltip title={t('common.delete')}>
            <IconButton
              size="small"
              color="error"
              onClick={onDelete}
              sx={{
                bgcolor: theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.08)' : 'rgba(211, 47, 47, 0.08)'
              }}
            >
              <DeleteIcon fontSize="small" />
            </IconButton>
          </Tooltip>
        </CardActions>
      </Card>

      {/* 编辑文本块对话框 */}
      <EditChunkDialog
        open={editDialogOpen}
        chunk={chunkForEdit || chunk}
        onClose={() => {
          setEditDialogOpen(false);
          setChunkForEdit(null);
        }}
        onSave={handleSaveEdit}
      />
    </>
  );
}

'use client';

import { Box, Typography, Chip, Tooltip, alpha, CircularProgress } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { useTheme } from '@mui/material/styles';
import { useState } from 'react';

/**
 * 数据集元数据展示组件
 */
export default function DatasetMetadata({ currentDataset, onViewChunk }) {
  const { t } = useTranslation();
  const theme = useTheme();

  return (
    <Box sx={{ mb: 3 }}>
      <Typography variant="subtitle1" color="text.secondary" sx={{ mb: 1 }}>
        {t('datasets.metadata')}
      </Typography>
      <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
        <Chip label={`${t('datasets.model')}: ${currentDataset.model}`} variant="outlined" />
        {currentDataset.questionLabel && (
          <Chip label={`${t('common.label')}: ${currentDataset.questionLabel}`} color="primary" variant="outlined" />
        )}
        <Chip
          label={`${t('datasets.createdAt')}: ${new Date(currentDataset.createAt).toLocaleString('zh-CN')}`}
          variant="outlined"
        />
        <Tooltip title={t('textSplit.viewChunk')}>
          <Chip
            label={`${t('datasets.chunkId')}: ${currentDataset.chunkName}`}
            variant="outlined"
            color="info"
            onClick={async () => {
              try {
                // 使用新API接口获取文本块内容
                const response = await fetch(
                  `/api/projects/${currentDataset.projectId}/chunks/name?chunkName=${encodeURIComponent(currentDataset.chunkName)}`
                );

                if (!response.ok) {
                  throw new Error(`获取文本块失败: ${response.statusText}`);
                }

                const chunkData = await response.json();

                // 调用父组件的方法显示文本块
                onViewChunk({
                  name: currentDataset.chunkName,
                  content: chunkData.content
                });
              } catch (error) {
                console.error('获取文本块内容失败:', error);
                // 即使API请求失败，也尝试调用查看方法
                onViewChunk({
                  name: currentDataset.chunkName,
                  content: '内容加载失败，请重试'
                });
              }
            }}
            sx={{ cursor: 'pointer' }}
          />
        </Tooltip>
        {currentDataset.confirmed && (
          <Chip
            label={t('datasets.confirmed')}
            sx={{
              backgroundColor: alpha(theme.palette.success.main, 0.1),
              color: theme.palette.success.dark,
              fontWeight: 'medium'
            }}
          />
        )}
      </Box>
    </Box>
  );
}

import { NextResponse } from 'next/server';
import { deleteChunkById, getChunkByFileIds, getChunkById, getChunksByFileIds, updateChunkById } from '@/lib/db/chunks';

// 获取文本块内容
export async function POST(request, { params }) {
  try {
    const { projectId } = params;
    // 验证参数
    if (!projectId) {
      return NextResponse.json({ error: 'Project ID cannot be empty' }, { status: 400 });
    }
    const { array } = await request.json();
    // 获取文本块内容
    const chunk = await getChunksByFileIds(array);

    return NextResponse.json(chunk);
  } catch (error) {
    console.error('Failed to get text block content:', String(error));
    return NextResponse.json({ error: String(error) || 'Failed to get text block content' }, { status: 500 });
  }
}

'use server';

import fs from 'fs';
import path from 'path';
import os from 'os';

// 获取适合的数据存储目录
function getDbDirectory() {
  if (process.resourcesPath) {
    const rootPath = String(fs.readFileSync(path.join(process.resourcesPath, 'root-path.txt')));
    if (rootPath) {
      return rootPath;
    }
  }
  // 检查是否在浏览器环境中运行
  if (typeof window !== 'undefined') {
    // 检查是否在 Electron 渲染进程中运行
    if (window.electron && window.electron.getUserDataPath) {
      // 使用 preload 脚本中暴露的 API 获取用户数据目录
      const userDataPath = window.electron.getUserDataPath();
      if (userDataPath) {
        return path.join(userDataPath, 'local-db');
      }
    }

    // 如果不是 Electron 或获取失败，则使用开发环境的路径
    return path.join(process.cwd(), 'local-db');
  } else if (process.versions && process.versions.electron) {
    // 在 Electron 主进程中运行
    try {
      const { app } = require('electron');
      return path.join(app.getPath('userData'), 'local-db');
    } catch (error) {
      console.error('Failed to get user data directory:', String(error), path.join(os.homedir(), '.easy-dataset-db'));
      // 降级处理，使用临时目录
      return path.join(os.homedir(), '.easy-dataset-db');
    }
  } else {
    // 在普通 Node.js 环境中运行（开发模式）
    return path.join(process.cwd(), 'local-db');
  }
}

let PROJECT_ROOT = '';

// 获取项目根目录
export async function getProjectRoot() {
  if (!PROJECT_ROOT) {
    PROJECT_ROOT = getDbDirectory();
  }
  return PROJECT_ROOT;
}

// 确保数据库目录存在
export async function ensureDbExists() {
  try {
    await fs.promises.access(PROJECT_ROOT);
  } catch (error) {
    await fs.promises.mkdir(PROJECT_ROOT, { recursive: true });
  }
}

// 读取JSON文件
export async function readJsonFile(filePath) {
  try {
    await fs.promises.access(filePath);
    const data = await fs.promises.readFile(filePath, 'utf8');
    return JSON.parse(data);
  } catch (error) {
    return null;
  }
}

// 写入JSON文件
export async function writeJsonFile(filePath, data) {
  // 使用临时文件策略，避免写入中断导致文件损坏
  const tempFilePath = `${filePath}_${Date.now()}.tmp`;
  try {
    // 序列化为JSON字符串
    const jsonString = JSON.stringify(data, null, 2);
    // 先写入临时文件
    await fs.promises.writeFile(tempFilePath, jsonString, 'utf8');

    // 从临时文件读取内容并验证
    try {
      const writtenContent = await fs.promises.readFile(tempFilePath, 'utf8');
      JSON.parse(writtenContent); // 验证JSON是否有效
      // 验证通过后，原子性地重命名文件替换原文件
      await fs.promises.rename(tempFilePath, filePath);
    } catch (validationError) {
      // 验证失败，删除临时文件并抛出错误
      await fs.promises.unlink(tempFilePath).catch(() => {});
      throw new Error(`写入的JSON文件内容无效: ${validationError.message}`);
    }
    return data;
  } catch (error) {
    console.error(`写入JSON文件 ${filePath} 失败:`, error);
    throw error;
  } finally {
    // 确保临时文件被删除
    await fs.promises.unlink(tempFilePath).catch(() => {});
  }
}

// 确保目录存在
export async function ensureDir(dirPath) {
  try {
    await fs.promises.access(dirPath);
  } catch (error) {
    await fs.promises.mkdir(dirPath, { recursive: true });
  }
}

// HuggingFaceTab.js 组件
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
  Typography,
  Box,
  TextField,
  Button,
  FormControlLabel,
  Checkbox,
  Alert,
  CircularProgress,
  Divider,
  Paper,
  Grid,
  Tooltip,
  IconButton,
  Link
} from '@mui/material';
import InfoIcon from '@mui/icons-material/Info';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';

const HuggingFaceTab = ({
  projectId,
  systemPrompt,
  confirmedOnly,
  includeCOT,
  formatType,
  fileFormat,
  customFields,
  handleSystemPromptChange,
  handleConfirmedOnlyChange,
  handleIncludeCOTChange
}) => {
  const { t } = useTranslation();
  const [token, setToken] = useState('');
  const [datasetName, setDatasetName] = useState('');
  const [isPrivate, setIsPrivate] = useState(false);
  const [uploading, setUploading] = useState(false);
  const [error, setError] = useState('');
  const [success, setSuccess] = useState(false);
  const [datasetUrl, setDatasetUrl] = useState('');
  const [hasToken, setHasToken] = useState(false);
  const [loading, setLoading] = useState(true);

  // 从配置中获取 huggingfaceToken
  useEffect(() => {
    if (projectId) {
      setLoading(true);
      fetch(`/api/projects/${projectId}/config`)
        .then(res => res.json())
        .then(data => {
          if (data.huggingfaceToken) {
            setToken(data.huggingfaceToken);
            setHasToken(true);
          }
          setLoading(false);
        })
        .catch(err => {
          console.error('获取 HuggingFace Token 失败:', err);
          setLoading(false);
        });
    }
  }, [projectId]);

  // 处理上传数据集到 HuggingFace
  const handleUpload = async () => {
    if (!hasToken) {
      return;
    }

    if (!datasetName) {
      setError('请输入数据集名称');
      return;
    }

    try {
      setUploading(true);
      setError('');
      setSuccess(false);

      const response = await fetch(`/api/projects/${projectId}/huggingface/upload`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          token,
          datasetName,
          isPrivate,
          formatType,
          systemPrompt,
          confirmedOnly,
          includeCOT,
          fileFormat,
          customFields: formatType === 'custom' ? customFields : undefined
        })
      });

      const data = await response.json();
      if (!response.ok) {
        throw new Error(data.error || '上传失败');
      }

      setSuccess(true);
      setDatasetUrl(data.url);
    } catch (err) {
      setError(err.message);
    } finally {
      setUploading(false);
    }
  };

  return (
    <Box sx={{ mt: 2 }}>
      {error && (
        <Alert severity="error" sx={{ mb: 2 }}>
          {error}
        </Alert>
      )}

      {success && (
        <Alert severity="success" sx={{ mb: 2 }} icon={<CheckCircleOutlineIcon fontSize="inherit" />}>
          {t('export.uploadSuccess')}
          {datasetUrl && (
            <Box mt={1}>
              <Link href={datasetUrl} target="_blank" rel="noopener noreferrer">
                {t('export.viewOnHuggingFace')}
              </Link>
            </Box>
          )}
        </Alert>
      )}

      {!hasToken ? (
        <Alert severity="warning" sx={{ mb: 3 }}>
          {t('export.noTokenWarning')}
          <Box mt={1}>
            <Button
              variant="outlined"
              size="small"
              onClick={() => (window.location.href = `/projects/${projectId}/settings`)}
            >
              {t('export.goToSettings')}
            </Button>
          </Box>
        </Alert>
      ) : null}

      <Divider sx={{ my: 2 }} />

      <Box sx={{ mb: 3 }}>
        <Typography variant="subtitle1" fontWeight="bold" gutterBottom>
          {t('export.datasetSettings')}
        </Typography>

        <Grid container spacing={2}>
          <Grid item xs={12}>
            <TextField
              fullWidth
              label={t('export.datasetName')}
              placeholder="username/dataset-name"
              value={datasetName}
              onChange={e => setDatasetName(e.target.value)}
              helperText={t('export.datasetNameHelp')}
              sx={{ mb: 2 }}
            />
          </Grid>

          <Grid item xs={12}>
            <FormControlLabel
              control={<Checkbox checked={isPrivate} onChange={e => setIsPrivate(e.target.checked)} />}
              label={t('export.privateDataset')}
            />
          </Grid>
        </Grid>
      </Box>

      <Box sx={{ mb: 3 }}>
        <Typography variant="subtitle1" fontWeight="bold" gutterBottom>
          {t('export.exportOptions')}
        </Typography>

        <Box sx={{ mb: 2 }}>
          <Typography variant="subtitle2" gutterBottom>
            {t('export.systemPrompt')}
          </Typography>
          <TextField
            fullWidth
            multiline
            rows={3}
            value={systemPrompt}
            onChange={handleSystemPromptChange}
            variant="outlined"
          />
        </Box>

        <Box sx={{ display: 'flex', flexDirection: 'row', gap: 4 }}>
          <FormControlLabel
            control={<Checkbox checked={confirmedOnly} onChange={handleConfirmedOnlyChange} />}
            label={t('export.onlyConfirmed')}
          />

          <FormControlLabel
            control={<Checkbox checked={includeCOT} onChange={handleIncludeCOTChange} />}
            label={t('export.includeCOT')}
          />
        </Box>
      </Box>

      <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 3 }}>
        <Button
          variant="contained"
          onClick={handleUpload}
          disabled={uploading || !hasToken || !datasetName}
          sx={{ borderRadius: 2 }}
        >
          {uploading ? <CircularProgress size={24} /> : t('export.uploadToHuggingFace')}
        </Button>
      </Box>
    </Box>
  );
};

export default HuggingFaceTab;

import { defaultProcessing } from './default';
import { minerUProcessing } from './mineru';
import { visionProcessing } from './vision';

/**
 * PDF处理服务入口
 * @param {string} projectId 项目ID
 * @param {string} fileName 文件名
 * @param {Object} options 处理选项
 * @param {string} strategy 处理策略，可选值: 'default', 'mineru', 'vision'
 * @returns {Promise<Object>} 处理结果
 */
export async function processPdf(strategy = 'default', projectId, fileName, options = {}) {
  switch (strategy.toLowerCase()) {
    case 'default':
      return await defaultProcessing(projectId, fileName, options);
    case 'mineru':
      return await minerUProcessing(projectId, fileName, options);
    case 'vision':
      return await visionProcessing(projectId, fileName, options);
    default:
      throw new Error(`unsupported PDF processing strategy: ${strategy}`);
  }
}

export default {
  processPdf
};

export * from './util';

'use client';

import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import request from '@/lib/util/request';

export function useQuestionEdit(projectId, onSuccess) {
  const { t } = useTranslation();
  const [editDialogOpen, setEditDialogOpen] = useState(false);
  const [editMode, setEditMode] = useState('create');
  const [editingQuestion, setEditingQuestion] = useState(null);

  const handleOpenCreateDialog = () => {
    setEditMode('create');
    setEditingQuestion(null);
    setEditDialogOpen(true);
  };

  const handleOpenEditDialog = question => {
    setEditMode('edit');
    setEditingQuestion(question);
    setEditDialogOpen(true);
  };

  const handleCloseDialog = () => {
    setEditDialogOpen(false);
    setEditingQuestion(null);
  };

  const handleSubmitQuestion = async formData => {
    try {
      const response = await request(`/api/projects/${projectId}/questions`, {
        method: editMode === 'create' ? 'POST' : 'PUT',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(
          editMode === 'create'
            ? {
                question: formData.question,
                chunkId: formData.chunkId,
                label: formData.label
              }
            : {
                id: formData.id,
                question: formData.question,
                chunkId: formData.chunkId,
                label: formData.label
              }
        )
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.error || t('questions.operationFailed'));
      }

      // 获取更新后的问题数据
      const updatedQuestion = await response.json();

      // 直接更新问题列表中的数据，而不是重新获取整个列表
      if (onSuccess) {
        onSuccess(updatedQuestion);
      }
      handleCloseDialog();
    } catch (error) {
      console.error('操作失败:', error);
    }
  };

  return {
    editDialogOpen,
    editMode,
    editingQuestion,
    handleOpenCreateDialog,
    handleOpenEditDialog,
    handleCloseDialog,
    handleSubmitQuestion
  };
}

import { NextResponse } from 'next/server';
import path from 'path';
import fs from 'fs';

// 获取当前版本
function getCurrentVersion() {
  try {
    const packageJsonPath = path.join(process.cwd(), 'package.json');
    const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
    return packageJson.version;
  } catch (error) {
    console.error('读取版本信息失败:', String(error));
    return '1.0.0';
  }
}

// 从 GitHub 获取最新版本
async function getLatestVersion() {
  try {
    const owner = 'ConardLi';
    const repo = 'easy-dataset';
    const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`);

    if (!response.ok) {
      throw new Error(`GitHub API 请求失败: ${response.status}`);
    }

    const data = await response.json();
    return data.tag_name.replace('v', '');
  } catch (error) {
    console.error('获取最新版本失败:', String(error));
    return null;
  }
}

// 检查是否有更新
export async function GET() {
  try {
    const currentVersion = getCurrentVersion();
    const latestVersion = await getLatestVersion();

    if (!latestVersion) {
      return NextResponse.json({
        hasUpdate: false,
        currentVersion,
        latestVersion: null,
        error: '获取最新版本失败'
      });
    }

    // 简单的版本比较
    const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;

    return NextResponse.json({
      hasUpdate,
      currentVersion,
      latestVersion,
      releaseUrl: hasUpdate ? `https://github.com/ConardLi/easy-dataset/releases/tag/v${latestVersion}` : null
    });
  } catch (error) {
    console.error('检查更新失败:', String(error));
  }
}

// 简单的版本比较函数
function compareVersions(a, b) {
  const partsA = a.split('.').map(Number);
  const partsB = b.split('.').map(Number);

  for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
    const numA = i < partsA.length ? partsA[i] : 0;
    const numB = i < partsB.length ? partsB[i] : 0;

    if (numA > numB) return 1;
    if (numA < numB) return -1;
  }

  return 0;
}

'use client';

import {
  Dialog,
  DialogTitle,
  DialogContent,
  Card,
  CardContent,
  Typography,
  Box,
  Stack,
  FormControl,
  InputLabel,
  Select,
  MenuItem
} from '@mui/material';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { styled } from '@mui/material/styles';
import ArticleOutlinedIcon from '@mui/icons-material/ArticleOutlined';
import ScienceOutlinedIcon from '@mui/icons-material/ScienceOutlined';
import LaunchOutlinedIcon from '@mui/icons-material/LaunchOutlined';
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';

const StyledCard = styled(Card)(({ theme, disabled }) => ({
  cursor: disabled ? 'not-allowed' : 'pointer',
  opacity: disabled ? 0.6 : 1,
  transition: 'transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out',
  '&:hover': disabled
    ? {}
    : {
        transform: 'translateY(-4px)',
        boxShadow: theme.shadows[4]
      }
}));

const OptionCard = ({
  icon,
  title,
  description,
  disabled,
  onClick,
  selected,
  isVisionEnabled,
  visionModels,
  selectorName,
  handleSettingChange,
  selectedViosnModel
}) => (
  <StyledCard
    disabled={disabled}
    onClick={disabled ? undefined : onClick}
    sx={{
      height: '100%',
      border: selected ? '2px solid primary.main' : '1px solid divider',
      backgroundColor: selected ? 'action.selected' : 'background.paper'
    }}
  >
    <CardContent>
      <Stack spacing={1}>
        <Box sx={{ color: 'primary.main', mb: 1 }}>{icon}</Box>
        <Typography variant="h6" component="div">
          {title}
        </Typography>
        <Typography variant="body2" color="text.secondary">
          {description}
        </Typography>
        {isVisionEnabled && (
          <FormControl fullWidth>
            <InputLabel>{selectorName}</InputLabel>
            <Select
              label={selectorName}
              value={selectedViosnModel}
              onChange={e => handleSettingChange(e)}
              name="vision"
            >
              {visionModels.map(item => (
                <MenuItem key={item.id} value={item.id}>
                  {item.modelName} ({item.providerName})
                </MenuItem>
              ))}
            </Select>
          </FormControl>
        )}
      </Stack>
    </CardContent>
  </StyledCard>
);

export default function PdfProcessingDialog({
  open,
  onClose,
  onRadioChange,
  value,
  taskSettings,
  visionModels,
  selectedViosnModel,
  setSelectedViosnModel
}) {
  const { t } = useTranslation();

  //检查配置中是否启用MinerU
  const isMinerUEnabled = taskSettings && taskSettings.minerUToken ? true : false;

  //检查配置中是否启用Vision策略
  const isVisionEnabled = visionModels.length > 0 ? true : false;

  //用于传递到父组件，显示当前选中的模型
  let selectedModel = selectedViosnModel;

  const handleOptionClick = optionValue => {
    if (optionValue === 'mineru-web') {
      window.open('https://mineru.net/OpenSourceTools/Extractor', '_blank');
    } else {
      onRadioChange({ target: { value: optionValue, selectedVision: selectedModel } });
      onClose();
    }
  };

  // 处理设置变更
  const handleSettingChange = e => {
    const { value } = e.target;
    selectedModel = value;
    setSelectedViosnModel(value);
  };

  return (
    <Dialog open={open} onClose={onClose} fullWidth maxWidth="md">
      <DialogTitle>{t('textSplit.pdfProcess')}</DialogTitle>
      <DialogContent>
        <Box
          sx={{
            display: 'grid',
            gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
            gap: 2,
            p: 1
          }}
        >
          <OptionCard
            icon={<ArticleOutlinedIcon fontSize="large" />}
            title={t('textSplit.basicPdfParsing')}
            description={t('textSplit.basicPdfParsingDesc')}
            onClick={() => handleOptionClick('default')}
            selected={value === 'default'}
          />
          <OptionCard
            icon={<ScienceOutlinedIcon fontSize="large" />}
            title="MinerU API"
            description={isMinerUEnabled ? t('textSplit.mineruApiDesc') : t('textSplit.mineruApiDescDisabled')}
            disabled={!isMinerUEnabled}
            onClick={() => handleOptionClick('mineru')}
            selected={value === 'mineru'}
          />
          <OptionCard
            icon={<LaunchOutlinedIcon fontSize="large" />}
            title={t('textSplit.mineruWebPlatform')}
            description={t('textSplit.mineruWebPlatformDesc')}
            onClick={() => handleOptionClick('mineru-web')}
          />
          <OptionCard
            icon={<SmartToyOutlinedIcon fontSize="large" />}
            title={t('textSplit.customVisionModel')}
            description={t('textSplit.customVisionModelDesc')}
            disabled={!isVisionEnabled}
            onClick={() => handleOptionClick('vision')}
            selected={value === 'vision'}
            isVisionEnabled={isVisionEnabled}
            visionModels={visionModels}
            selectorName={t('settings.vision')}
            selectedViosnModel={selectedViosnModel}
            handleSettingChange={handleSettingChange}
          />
        </Box>
      </DialogContent>
    </Dialog>
  );
}

import { FILE } from '@/constant';

/**
 * 检查文件大小
 */
export function checkMaxSize(files) {
  const oversizedFiles = files.filter(file => file.size > FILE.MAX_FILE_SIZE);
  if (oversizedFiles.length > 0) {
    throw new Error(`Max 50MB: ${oversizedFiles.map(f => f.name).join(', ')}`);
  }
}

/**
 * 获取可以上传的文件
 * @param {*} files
 * @returns
 */
export function getvalidFiles(files) {
  return files.filter(
    file =>
      file.name.endsWith('.md') ||
      file.name.endsWith('.txt') ||
      file.name.endsWith('.docx') ||
      file.name.endsWith('.pdf')
  );
}

/**
 * 检查不能上传的文件
 * @param {*} files
 * @returns
 */
export function checkInvalidFiles(files) {
  const invalidFiles = files.filter(
    file =>
      !file.name.endsWith('.md') &&
      !file.name.endsWith('.txt') &&
      !file.name.endsWith('.docx') &&
      !file.name.endsWith('.pdf')
  );
  if (invalidFiles.length > 0) {
    throw new Error(`Unsupported File Format: ${invalidFiles.map(f => f.name).join(', ')}`);
  }
  return invalidFiles;
}

import { getProjectRoot } from '@/lib/db/base';
import { NextResponse } from 'next/server';
import path from 'path';
import fs from 'fs';
import { promisify } from 'util';

const rmdir = promisify(fs.rm);

/**
 * 删除项目目录
 * @returns {Promise<Response>} 操作结果响应
 */
export async function POST(request) {
  try {
    const { projectId } = await request.json();

    if (!projectId) {
      return NextResponse.json(
        {
          success: false,
          error: '项目ID不能为空'
        },
        { status: 400 }
      );
    }

    // 获取项目根目录
    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);

    // 检查目录是否存在
    if (!fs.existsSync(projectPath)) {
      return NextResponse.json(
        {
          success: false,
          error: '项目目录不存在'
        },
        { status: 404 }
      );
    }

    // 递归删除目录
    await rmdir(projectPath, { recursive: true, force: true });

    return NextResponse.json({
      success: true,
      message: '项目目录已删除'
    });
  } catch (error) {
    console.error('删除项目目录出错:', String(error));
    return NextResponse.json(
      {
        success: false,
        error: error.message
      },
      { status: 500 }
    );
  }
}

'use client';

import React, { useState } from 'react';
import { Box, TextField, Button, IconButton, Badge, Tooltip } from '@mui/material';
import SendIcon from '@mui/icons-material/Send';
import ImageIcon from '@mui/icons-material/Image';
import CancelIcon from '@mui/icons-material/Cancel';
import { useTheme } from '@mui/material/styles';
import { playgroundStyles } from '@/styles/playground';
import { useTranslation } from 'react-i18next';

const MessageInput = ({
  userInput,
  handleInputChange,
  handleSendMessage,
  loading,
  selectedModels,
  uploadedImage,
  handleImageUpload,
  handleRemoveImage,
  availableModels
}) => {
  const theme = useTheme();
  const styles = playgroundStyles(theme);
  const { t } = useTranslation();

  const isDisabled = Object.values(loading).some(value => value) || selectedModels.length === 0;
  const isSendDisabled = isDisabled || (!userInput.trim() && !uploadedImage);

  // 检查是否有视觉模型被选中
  const hasVisionModel = selectedModels.some(modelId => {
    const model = availableModels.find(m => m.id === modelId);
    return model && model.type === 'vision';
  });

  return (
    <Box sx={styles.inputContainer}>
      {uploadedImage && (
        <Box sx={{ position: 'relative', mb: 1, display: 'inline-block', maxWidth: '100%' }}>
          <Badge
            badgeContent={
              <IconButton
                size="small"
                onClick={handleRemoveImage}
                sx={{ bgcolor: 'rgba(0, 0, 0, 0.4)', '&:hover': { bgcolor: 'rgba(0, 0, 0, 0.6)' } }}
              >
                <CancelIcon fontSize="small" sx={{ color: '#fff' }} />
              </IconButton>
            }
            sx={{ width: '100%' }}
            overlap="rectangular"
            anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
          >
            <img
              src={uploadedImage}
              alt="上传图片"
              style={{ maxWidth: '100%', maxHeight: '200px', borderRadius: '4px' }}
            />
          </Badge>
        </Box>
      )}
      <Box sx={{ display: 'flex', alignItems: 'flex-start', width: '100%' }}>
        <TextField
          fullWidth
          variant="outlined"
          placeholder={t('playground.inputMessage')}
          value={userInput}
          onChange={handleInputChange}
          disabled={isDisabled}
          onKeyPress={e => {
            if (e.key === 'Enter' && !e.shiftKey) {
              e.preventDefault();
              handleSendMessage();
            }
          }}
          multiline
          maxRows={4}
        />
        {hasVisionModel && (
          <Tooltip title={t('playground.uploadImage')}>
            <span>
              <IconButton color="primary" component="label" disabled={isDisabled} sx={{ ml: 1, mr: 1 }}>
                <input hidden accept="image/*" type="file" onChange={handleImageUpload} />
                <ImageIcon />
              </IconButton>
            </span>
          </Tooltip>
        )}
        <Button
          variant="contained"
          color="primary"
          endIcon={<SendIcon />}
          onClick={handleSendMessage}
          disabled={isSendDisabled}
          sx={styles.sendButton}
        >
          {t('playground.send')}
        </Button>
      </Box>
    </Box>
  );
};

export default MessageInput;

const { Menu, dialog, shell, app } = require('electron');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { getAppVersion } = require('../util');

/**
 * 创建应用菜单
 * @param {BrowserWindow} mainWindow 主窗口
 * @param {Function} clearCache 清除缓存函数
 */
function createMenu(mainWindow, clearCache) {
  const template = [
    {
      label: 'File',
      submenu: [{ role: 'quit', label: 'Quit' }]
    },
    {
      label: 'Edit',
      submenu: [
        { role: 'undo', label: 'Undo' },
        { role: 'redo', label: 'Redo' },
        { type: 'separator' },
        { role: 'cut', label: 'Cut' },
        { role: 'copy', label: 'Copy' },
        { role: 'paste', label: 'Paste' }
      ]
    },
    {
      label: 'View',
      submenu: [
        { role: 'reload', label: 'Refresh' },
        { type: 'separator' },
        { role: 'resetzoom', label: 'Reset Zoom' },
        { role: 'zoomin', label: 'Zoom In' },
        { role: 'zoomout', label: 'Zoom Out' },
        { type: 'separator' },
        { role: 'togglefullscreen', label: 'Fullscreen' }
      ]
    },
    {
      label: 'Help',
      submenu: [
        {
          label: 'About',
          click: () => {
            dialog.showMessageBox(mainWindow, {
              title: 'About Easy Dataset',
              message: `Easy Dataset v${getAppVersion()}`,
              detail: 'An application for creating fine-tuning datasets for large models.',
              buttons: ['OK']
            });
          }
        },
        {
          label: 'Visit GitHub',
          click: () => {
            shell.openExternal('https://github.com/ConardLi/easy-dataset');
          }
        }
      ]
    },
    {
      label: 'More',
      submenu: [
        { role: 'toggledevtools', label: 'Developer Tools' },
        {
          label: 'Open Logs Directory',
          click: () => {
            const logsDir = path.join(app.getPath('userData'), 'logs');
            if (!fs.existsSync(logsDir)) {
              fs.mkdirSync(logsDir, { recursive: true });
            }
            shell.openPath(logsDir);
          }
        },
        {
          label: 'Open Data Directory',
          click: () => {
            const dataDir = path.join(app.getPath('userData'), 'local-db');
            if (!fs.existsSync(dataDir)) {
              fs.mkdirSync(dataDir, { recursive: true });
            }
            shell.openPath(dataDir);
          }
        },
        {
          label: 'Open Data Directory (History)',
          click: () => {
            const dataDir = path.join(os.homedir(), '.easy-dataset-db');
            if (!fs.existsSync(dataDir)) {
              fs.mkdirSync(dataDir, { recursive: true });
            }
            shell.openPath(dataDir);
          }
        },
        {
          label: 'Clear Cache',
          click: async () => {
            try {
              const response = await dialog.showMessageBox(mainWindow, {
                type: 'question',
                buttons: ['Cancel', 'Confirm'],
                defaultId: 1,
                title: 'Clear Cache',
                message: 'Are you sure you want to clear the cache?',
                detail:
                  'This will delete all files in the logs directory and local database cache files (excluding main database files).'
              });

              if (response.response === 1) {
                // User clicked confirm
                await clearCache();
                dialog.showMessageBox(mainWindow, {
                  type: 'info',
                  title: 'Cleared Successfully',
                  message: 'Cache has been cleared successfully'
                });
              }
            } catch (error) {
              global.appLog(`Failed to clear cache: ${error.message}`, 'error');
              dialog.showErrorBox('Failed to clear cache', error.message);
            }
          }
        }
      ]
    }
  ];

  const menu = Menu.buildFromTemplate(template);
  Menu.setApplicationMenu(menu);
}

module.exports = {
  createMenu
};

import { NextResponse } from 'next/server';
import { deleteChunkById, getChunkById, updateChunkById } from '@/lib/db/chunks';

// 获取文本块内容
export async function GET(request, { params }) {
  try {
    const { projectId, chunkId } = params;
    // 验证参数
    if (!projectId) {
      return NextResponse.json({ error: 'Project ID cannot be empty' }, { status: 400 });
    }
    if (!chunkId) {
      return NextResponse.json({ error: 'Text block ID cannot be empty' }, { status: 400 });
    }
    // 获取文本块内容
    const chunk = await getChunkById(chunkId);

    return NextResponse.json(chunk);
  } catch (error) {
    console.error('Failed to get text block content:', String(error));
    return NextResponse.json({ error: error.message || 'Failed to get text block content' }, { status: 500 });
  }
}

// 删除文本块
export async function DELETE(request, { params }) {
  try {
    const { projectId, chunkId } = params;
    // 验证参数
    if (!projectId) {
      return NextResponse.json({ error: 'Project ID cannot be empty' }, { status: 400 });
    }
    if (!chunkId) {
      return NextResponse.json({ error: 'Text block ID cannot be empty' }, { status: 400 });
    }
    await deleteChunkById(chunkId);

    return NextResponse.json({ message: 'Text block deleted successfully' });
  } catch (error) {
    console.error('Failed to delete text block:', String(error));
    return NextResponse.json({ error: error.message || 'Failed to delete text block' }, { status: 500 });
  }
}

// 编辑文本块内容
export async function PATCH(request, { params }) {
  try {
    const { projectId, chunkId } = params;

    // 验证参数
    if (!projectId) {
      return NextResponse.json({ error: '项目ID不能为空' }, { status: 400 });
    }

    if (!chunkId) {
      return NextResponse.json({ error: '文本块ID不能为空' }, { status: 400 });
    }

    // 解析请求体获取新内容
    const requestData = await request.json();
    const { content } = requestData;

    if (!content) {
      return NextResponse.json({ error: '内容不能为空' }, { status: 400 });
    }

    let res = await updateChunkById(chunkId, { content });
    return NextResponse.json(res);
  } catch (error) {
    console.error('编辑文本块失败:', String(error));
    return NextResponse.json({ error: error.message || '编辑文本块失败' }, { status: 500 });
  }
}

'use client';

import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import axios from 'axios';
import { toast } from 'sonner';
import { useSetAtom } from 'jotai/index';
import { modelConfigListAtom, selectedModelInfoAtom } from '@/lib/store';

export default function ProjectPage({ params }) {
  const router = useRouter();
  const setConfigList = useSetAtom(modelConfigListAtom);
  const setSelectedModelInfo = useSetAtom(selectedModelInfoAtom);
  const { projectId } = params;

  // 默认重定向到文本分割页面
  useEffect(() => {
    getModelConfigList(projectId);
    router.push(`/projects/${projectId}/text-split`);
  }, [projectId, router]);

  const getModelConfigList = projectId => {
    axios
      .get(`/api/projects/${projectId}/model-config`)
      .then(response => {
        setConfigList(response.data.data);
        if (response.data.defaultModelConfigId) {
          setSelectedModelInfo(response.data.data.find(item => item.id === response.data.defaultModelConfigId));
        } else {
          setSelectedModelInfo('');
        }
      })
      .catch(error => {
        toast.error('get model list error');
      });
  };

  return null;
}

'use client';

import { useState } from 'react';
import {
  Box,
  Typography,
  IconButton,
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableHead,
  TableRow,
  Chip,
  Divider,
  useTheme,
  alpha,
  Tooltip,
  Checkbox,
  TablePagination,
  Card,
  TextField
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import VisibilityIcon from '@mui/icons-material/Visibility';
import { useTranslation } from 'react-i18next';

// 数据集列表组件
const DatasetList = ({
  datasets,
  onViewDetails,
  onDelete,
  page,
  rowsPerPage,
  onPageChange,
  onRowsPerPageChange,
  total,
  selectedIds,
  onSelectAll,
  onSelectItem
}) => {
  const theme = useTheme();
  const { t } = useTranslation();
  const bgColor = theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.light;
  const color =
    theme.palette.mode === 'dark'
      ? theme.palette.getContrastText(theme.palette.primary.main)
      : theme.palette.getContrastText(theme.palette.primary.contrastText);
  return (
    <Card elevation={2}>
      <TableContainer sx={{ overflowX: 'auto' }}>
        <Table sx={{ minWidth: 750 }}>
          <TableHead>
            <TableRow>
              <TableCell
                padding="checkbox"
                sx={{
                  backgroundColor: bgColor,
                  color: color,
                  borderBottom: `2px solid ${theme.palette.divider}`
                }}
              >
                <Checkbox
                  color="primary"
                  indeterminate={selectedIds.length > 0 && selectedIds.length < total}
                  checked={total > 0 && selectedIds.length === total}
                  onChange={onSelectAll}
                />
              </TableCell>
              <TableCell
                sx={{
                  backgroundColor: bgColor,
                  color: color,
                  fontWeight: 'bold',
                  padding: '16px 8px',
                  borderBottom: `2px solid ${theme.palette.divider}`
                }}
              >
                {t('datasets.question')}
              </TableCell>
              <TableCell
                sx={{
                  backgroundColor: bgColor,
                  color: color,
                  fontWeight: 'bold',
                  padding: '16px 8px',
                  borderBottom: `2px solid ${theme.palette.divider}`
                }}
              >
                {t('datasets.createdAt')}
              </TableCell>
              <TableCell
                sx={{
                  backgroundColor: bgColor,
                  color: color,
                  fontWeight: 'bold',
                  padding: '16px 8px',
                  borderBottom: `2px solid ${theme.palette.divider}`
                }}
              >
                {t('datasets.model')}
              </TableCell>
              <TableCell
                sx={{
                  backgroundColor: bgColor,
                  color: color,
                  fontWeight: 'bold',
                  padding: '16px 8px',
                  borderBottom: `2px solid ${theme.palette.divider}`
                }}
              >
                {t('datasets.domainTag')}
              </TableCell>
              <TableCell
                sx={{
                  backgroundColor: bgColor,
                  color: color,
                  fontWeight: 'bold',
                  padding: '16px 8px',
                  borderBottom: `2px solid ${theme.palette.divider}`
                }}
              >
                {t('datasets.cot')}
              </TableCell>
              <TableCell
                sx={{
                  backgroundColor: bgColor,
                  color: color,
                  fontWeight: 'bold',
                  padding: '16px 8px',
                  borderBottom: `2px solid ${theme.palette.divider}`
                }}
              >
                {t('datasets.answer')}
              </TableCell>
              {/* <TableCell
                  sx={{
                    backgroundColor: bgColor,
                    color: color,
                    fontWeight: 'bold',
                    padding: '16px 8px',
                    borderBottom: `2px solid ${theme.palette.divider}`
                  }}>
                {t('datasets.chunkId')}
              </TableCell> */}
              <TableCell
                sx={{
                  backgroundColor: bgColor,
                  color: color,
                  fontWeight: 'bold',
                  padding: '16px 8px',
                  borderBottom: `2px solid ${theme.palette.divider}`
                }}
              >
                {t('common.actions')}
              </TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {datasets.map((dataset, index) => (
              <TableRow
                key={dataset.id}
                sx={{
                  '&:nth-of-type(odd)': { backgroundColor: alpha(theme.palette.primary.light, 0.05) },
                  '&:hover': { backgroundColor: alpha(theme.palette.primary.light, 0.1) },
                  cursor: 'pointer'
                }}
                onClick={() => onViewDetails(dataset.id)}
              >
                <TableCell
                  padding="checkbox"
                  sx={{
                    borderLeft: `4px solid ${theme.palette.primary.main}`
                  }}
                >
                  <Checkbox
                    color="primary"
                    checked={selectedIds.includes(dataset.id)}
                    onChange={e => {
                      e.stopPropagation();
                      onSelectItem(dataset.id);
                    }}
                    onClick={e => e.stopPropagation()}
                  />
                </TableCell>
                <TableCell
                  sx={{
                    maxWidth: 300,
                    whiteSpace: 'normal',
                    wordBreak: 'break-word',
                    py: 2
                  }}
                >
                  <Typography variant="body2" fontWeight="medium">
                    {dataset.confirmed}
                    {dataset.confirmed && (
                      <Chip
                        label={t('datasets.confirmed')}
                        size="small"
                        sx={{
                          backgroundColor: alpha(theme.palette.success.main, 0.1),
                          color: theme.palette.success.dark,
                          fontWeight: 'medium',
                          verticalAlign: 'baseline',
                          marginRight: '2px'
                        }}
                      />
                    )}
                    {dataset.question}
                  </Typography>
                </TableCell>
                <TableCell>
                  <Typography variant="body2" color="text.secondary">
                    {new Date(dataset.createAt).toLocaleString('zh-CN')}
                  </Typography>
                </TableCell>
                <TableCell>
                  <Chip
                    label={dataset.model}
                    size="small"
                    sx={{
                      backgroundColor: alpha(theme.palette.info.main, 0.1),
                      color: theme.palette.info.dark,
                      fontWeight: 'medium'
                    }}
                  />
                </TableCell>
                <TableCell>
                  {dataset.questionLabel ? (
                    <Chip
                      label={dataset.questionLabel}
                      size="small"
                      sx={{
                        backgroundColor: alpha(theme.palette.primary.main, 0.1),
                        color: theme.palette.primary.dark,
                        fontWeight: 'medium'
                      }}
                    />
                  ) : (
                    <Typography variant="body2" color="text.disabled">
                      {t('datasets.noTag')}
                    </Typography>
                  )}
                </TableCell>
                <TableCell>
                  <Chip
                    label={dataset.cot ? t('common.yes') : t('common.no')}
                    size="small"
                    sx={{
                      backgroundColor: dataset.cot
                        ? alpha(theme.palette.success.main, 0.1)
                        : alpha(theme.palette.grey[500], 0.1),
                      color: dataset.cot ? theme.palette.success.dark : theme.palette.grey[700],
                      fontWeight: 'medium'
                    }}
                  />
                </TableCell>
                <TableCell sx={{ maxWidth: 200 }}>
                  <Typography
                    variant="body2"
                    sx={{
                      overflow: 'hidden',
                      textOverflow: 'ellipsis',
                      display: '-webkit-box',
                      WebkitLineClamp: 2,
                      WebkitBoxOrient: 'vertical'
                    }}
                  >
                    {dataset.answer}
                  </Typography>
                </TableCell>
                {/* <TableCell sx={{ maxWidth: 200 }}>
                  <Typography
                    variant="body2"
                    sx={{
                      overflow: 'hidden',
                      textOverflow: 'ellipsis',
                      display: '-webkit-box',
                      WebkitLineClamp: 2,
                      WebkitBoxOrient: 'vertical'
                    }}>
                    {dataset.chunkId}
                  </Typography>
                </TableCell> */}
                <TableCell sx={{ width: 120 }}>
                  <Box sx={{ display: 'flex' }}>
                    <Tooltip title={t('datasets.viewDetails')}>
                      <IconButton
                        size="small"
                        onClick={e => {
                          e.stopPropagation();
                          onViewDetails(dataset.id);
                        }}
                        sx={{
                          color: theme.palette.primary.main,
                          '&:hover': { backgroundColor: alpha(theme.palette.primary.main, 0.1) }
                        }}
                      >
                        <VisibilityIcon fontSize="small" />
                      </IconButton>
                    </Tooltip>
                    <Tooltip title={t('common.delete')}>
                      <IconButton
                        size="small"
                        onClick={e => {
                          e.stopPropagation();
                          onDelete(dataset);
                        }}
                        sx={{
                          color: theme.palette.error.main,
                          '&:hover': { backgroundColor: alpha(theme.palette.error.main, 0.1) }
                        }}
                      >
                        <DeleteIcon fontSize="small" />
                      </IconButton>
                    </Tooltip>
                  </Box>
                </TableCell>
              </TableRow>
            ))}
            {datasets.length === 0 && (
              <TableRow>
                <TableCell colSpan={7} align="center" sx={{ py: 6 }}>
                  <Typography variant="body1" color="text.secondary">
                    {t('datasets.noData')}
                  </Typography>
                </TableCell>
              </TableRow>
            )}
          </TableBody>
        </Table>
      </TableContainer>
      <Divider />
      <Box
        sx={{
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between',
          px: 2,
          py: 1,
          borderTop: `1px solid ${theme.palette.divider}`
        }}
      >
        <TablePagination
          component="div"
          count={total}
          page={page - 1}
          onPageChange={onPageChange}
          rowsPerPage={rowsPerPage}
          onRowsPerPageChange={onRowsPerPageChange}
          labelRowsPerPage={t('datasets.rowsPerPage')}
          labelDisplayedRows={({ from, to, count }) => t('datasets.pagination', { from, to, count })}
          sx={{
            '.MuiTablePagination-selectLabel, .MuiTablePagination-displayedRows': {
              fontWeight: 'medium'
            },
            border: 'none'
          }}
        />
        <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
          <Typography variant="body2">{t('common.jumpTo')}:</Typography>
          <TextField
            size="small"
            type="number"
            inputProps={{
              min: 1,
              max: Math.ceil(total / rowsPerPage),
              style: { padding: '4px 8px', width: '50px' }
            }}
            onKeyPress={e => {
              if (e.key === 'Enter') {
                const pageNum = parseInt(e.target.value, 10);
                if (pageNum >= 1 && pageNum <= Math.ceil(total / rowsPerPage)) {
                  onPageChange(null, pageNum - 1);
                  e.target.value = '';
                }
              }
            }}
          />
        </Box>
      </Box>
    </Card>
  );
};

export default DatasetList;

'use client';

import {
  Grid,
  Paper,
  Button,
  Dialog,
  DialogTitle,
  DialogContent,
  DialogContentText,
  DialogActions,
  Typography
} from '@mui/material';
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import ProjectCard from './ProjectCard';

export default function ProjectList({ projects, onCreateProject }) {
  const { t } = useTranslation();
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  const [projectToDelete, setProjectToDelete] = useState(null);
  const [loading, setLoading] = useState(false);
  // 打开删除确认对话框
  const handleOpenDeleteDialog = (event, project) => {
    setProjectToDelete(project);
    setDeleteDialogOpen(true);
  };

  // 关闭删除确认对话框
  const handleCloseDeleteDialog = () => {
    setDeleteDialogOpen(false);
    setProjectToDelete(null);
  };

  // 删除项目
  const handleDeleteProject = async () => {
    if (!projectToDelete) return;

    try {
      setLoading(true);
      const response = await fetch(`/api/projects/${projectToDelete.id}`, {
        method: 'DELETE'
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.error || t('projects.deleteFailed'));
      }

      // 刷新页面以更新项目列表
      window.location.reload();
    } catch (error) {
      console.error('删除项目失败:', error);
      alert(error.message || t('projects.deleteFailed'));
    } finally {
      setLoading(false);
      handleCloseDeleteDialog();
    }
  };

  return (
    <>
      <Grid container spacing={3}>
        {projects.length === 0 ? (
          <Grid item xs={12}>
            <Paper sx={{ p: 4, textAlign: 'center' }}>
              <Typography variant="h6" color="text.secondary" gutterBottom>
                {t('projects.noProjects')}
              </Typography>
              <Button variant="contained" onClick={onCreateProject} startIcon={<AddCircleOutlineIcon />} sx={{ mt: 2 }}>
                {t('projects.createFirst')}
              </Button>
            </Paper>
          </Grid>
        ) : (
          projects.map(project => (
            <Grid item xs={12} sm={6} md={4} key={project.id}>
              <ProjectCard project={project} onDeleteClick={handleOpenDeleteDialog} />
            </Grid>
          ))
        )}
      </Grid>

      {/* 删除确认对话框 */}
      <Dialog
        open={deleteDialogOpen}
        onClose={handleCloseDeleteDialog}
        aria-labelledby="delete-dialog-title"
        aria-describedby="delete-dialog-description"
      >
        <DialogTitle id="delete-dialog-title">{t('projects.deleteConfirmTitle')}</DialogTitle>
        <DialogContent>
          <DialogContentText id="delete-dialog-description">
            {projectToDelete && (
              <>
                {t('projects.deleteConfirm')}
                <br />
                <Typography component="span" fontWeight="bold" sx={{ mt: 1, display: 'inline-block' }}>
                  {projectToDelete.name}
                </Typography>
              </>
            )}
          </DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button onClick={handleCloseDeleteDialog} disabled={loading}>
            {t('common.cancel')}
          </Button>
          <Button onClick={handleDeleteProject} color="error" variant="contained" disabled={loading}>
            {loading ? t('common.deleting') : t('common.delete')}
          </Button>
        </DialogActions>
      </Dialog>
    </>
  );
}

'use client';

import React from 'react';
import { IconButton, Tooltip } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import StopCircleIcon from '@mui/icons-material/StopCircle';
import { useTranslation } from 'react-i18next';

// 任务操作组件
export default function TaskActions({ task, onAbort, onDelete }) {
  const { t } = useTranslation();

  // 处理中的任务显示中断按钮，其他状态显示删除按钮
  return task.status === 0 ? (
    <Tooltip title={t('tasks.actions.abort')} arrow>
      <IconButton size="small" onClick={() => onAbort(task.id)}>
        <StopCircleIcon fontSize="small" color="warning" />
      </IconButton>
    </Tooltip>
  ) : (
    <Tooltip title={t('tasks.actions.delete')} arrow>
      <IconButton size="small" onClick={() => onDelete(task.id)}>
        <DeleteIcon fontSize="small" />
      </IconButton>
    </Tooltip>
  );
}

const { app, dialog, ipcMain } = require('electron');
const { setupLogging, setupIpcLogging } = require('./modules/logger');
const { createWindow, loadAppUrl, openDevTools, getMainWindow } = require('./modules/window-manager');
const { createMenu } = require('./modules/menu');
const { startNextServer } = require('./modules/server');
const { setupAutoUpdater } = require('./modules/updater');
const { initializeDatabase } = require('./modules/database');
const { clearCache } = require('./modules/cache');
const { setupIpcHandlers } = require('./modules/ipc-handlers');

// 是否是开发环境
const isDev = process.env.NODE_ENV === 'development';
const port = 1717;
let mainWindow;

// 当 Electron 完成初始化时创建窗口
app.whenReady().then(async () => {
  try {
    // 设置日志系统
    setupLogging(app);

    // 设置 IPC 处理程序
    setupIpcHandlers(app, isDev);
    setupIpcLogging(ipcMain, app, isDev);

    // 初始化数据库
    await initializeDatabase(app);

    // 创建主窗口
    mainWindow = createWindow(isDev, port);

    // 创建菜单
    createMenu(mainWindow, () => clearCache(app));

    // 在开发环境中加载 localhost URL
    if (isDev) {
      loadAppUrl(`http://localhost:${port}`);
      openDevTools();
    } else {
      // 在生产环境中启动 Next.js 服务
      const appUrl = await startNextServer(port, app);
      loadAppUrl(appUrl);
    }

    // 设置自动更新
    setupAutoUpdater(mainWindow);

    // 应用启动完成后的一段时间后自动检查更新
    setTimeout(() => {
      if (!isDev) {
        const { autoUpdater } = require('electron-updater');
        autoUpdater.checkForUpdates().catch(err => {
          console.error('Automatic update check failed:', err);
        });
      }
    }, 10000); // Check for updates after 10 seconds
  } catch (error) {
    console.error('An error occurred during application initialization:', error);
    dialog.showErrorBox(
      'Application Initialization Error',
      `An error occurred during startup, which may affect application functionality.
    Error details: ${error.message}`
    );
  }
});

// 当所有窗口关闭时退出应用
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    mainWindow = createWindow(isDev, port);
  }
});

// 应用退出前清理
app.on('before-quit', () => {
  console.log('应用正在退出...');
});

'use client';

import Navbar from '@/components/Navbar';
import { useState, useEffect } from 'react';
import { Box, CircularProgress, Typography, Button } from '@mui/material';
import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';

export default function ProjectLayout({ children, params }) {
  const router = useRouter();
  const { projectId } = params;
  const [projects, setProjects] = useState([]);
  const [currentProject, setCurrentProject] = useState(null);
  const [models, setModels] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [t] = useTranslation();
  // 定义获取数据的函数
  const fetchData = async () => {
    try {
      setLoading(true);

      // 获取用户创建的项目详情
      const projectsResponse = await fetch(`/api/projects`);
      if (!projectsResponse.ok) {
        throw new Error(t('projects.fetchFailed'));
      }
      const projectsData = await projectsResponse.json();
      setProjects(projectsData);

      // 获取当前项目详情
      const projectResponse = await fetch(`/api/projects/${projectId}`);
      if (!projectResponse.ok) {
        // 如果项目不存在，跳转到首页
        if (projectResponse.status === 404) {
          router.push('/');
          return;
        }
        throw new Error('获取项目详情失败');
      }
      const projectData = await projectResponse.json();
      setCurrentProject(projectData);
    } catch (error) {
      console.error('加载项目数据出错:', error);
      setError(error.message);
    } finally {
      setLoading(false);
    }
  };

  // 初始加载数据
  useEffect(() => {
    // 如果 projectId 是 undefined 或 "undefined"，直接重定向到首页
    if (!projectId || projectId === 'undefined') {
      router.push('/');
      return;
    }

    fetchData();
  }, [projectId, router]);

  if (loading) {
    return (
      <Box
        sx={{
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          height: '100vh'
        }}
      >
        <CircularProgress />
        <Typography sx={{ mt: 2 }}>加载项目数据...</Typography>
      </Box>
    );
  }

  if (error) {
    return (
      <Box
        sx={{
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          height: '100vh'
        }}
      >
        <Typography color="error">
          {t('projects.fetchFailed')}: {error}
        </Typography>
        <Button variant="contained" onClick={() => router.push('/')} sx={{ mt: 2 }}>
          {t('projects.backToHome')}
        </Button>
      </Box>
    );
  }

  return (
    <>
      <Navbar projects={projects} currentProject={projectId} />
      <main>{children}</main>
    </>
  );
}

import { NextResponse } from 'next/server';
import { getDatasetsById } from '@/lib/db/datasets';
import { getEncoding } from '@langchain/core/utils/tiktoken';

/**
 * 异步计算数据集文本的Token数量
 */
export async function GET(request, { params }) {
  try {
    const { projectId, datasetId } = params;

    if (!datasetId) {
      return NextResponse.json({ error: '数据集ID不能为空' }, { status: 400 });
    }

    const datasets = await getDatasetsById(datasetId);
    const tokenCounts = {
      answerTokens: 0,
      cotTokens: 0
    };

    try {
      if (datasets.answer || datasets.cot) {
        // 使用 cl100k_base 编码，适用于 gpt-3.5-turbo 和 gpt-4
        const encoding = await getEncoding('cl100k_base');

        if (datasets.answer) {
          const tokens = encoding.encode(datasets.answer);
          tokenCounts.answerTokens = tokens.length;
        }

        if (datasets.cot) {
          const tokens = encoding.encode(datasets.cot);
          tokenCounts.cotTokens = tokens.length;
        }
      }
    } catch (error) {
      console.error('计算Token数量失败:', String(error));
      return NextResponse.json({ error: '计算Token数量失败' }, { status: 500 });
    }

    return NextResponse.json(tokenCounts);
  } catch (error) {
    console.error('获取Token计数失败:', String(error));
    return NextResponse.json(
      {
        error: error.message || '获取Token计数失败'
      },
      { status: 500 }
    );
  }
}

// ExportDatasetDialog.js 组件
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Box, Tabs, Tab } from '@mui/material';

// 导入拆分后的组件
import LocalExportTab from './export/LocalExportTab';
import LlamaFactoryTab from './export/LlamaFactoryTab';
import HuggingFaceTab from './export/HuggingFaceTab';

const ExportDatasetDialog = ({ open, onClose, onExport, projectId }) => {
  const { t } = useTranslation();
  const [formatType, setFormatType] = useState('alpaca');
  const [systemPrompt, setSystemPrompt] = useState('');
  const [confirmedOnly, setConfirmedOnly] = useState(false);
  const [fileFormat, setFileFormat] = useState('json');
  const [includeCOT, setIncludeCOT] = useState(true);
  const [currentTab, setCurrentTab] = useState(0);
  // alpaca 格式特有的设置
  const [alpacaFieldType, setAlpacaFieldType] = useState('instruction'); // 'instruction' 或 'input'
  const [customInstruction, setCustomInstruction] = useState(''); // 当选择 input 时使用的自定义 instruction
  const [customFields, setCustomFields] = useState({
    questionField: 'instruction',
    answerField: 'output',
    cotField: 'complexCOT', // 添加思维链字段名
    includeLabels: false,
    includeChunk: false // 添加是否包含chunk字段
  });

  const handleFileFormatChange = event => {
    setFileFormat(event.target.value);
  };

  const handleFormatChange = event => {
    setFormatType(event.target.value);
    // 根据格式类型设置默认字段名
    if (event.target.value === 'alpaca') {
      setCustomFields({
        ...customFields,
        questionField: 'instruction',
        answerField: 'output'
      });
    } else if (event.target.value === 'sharegpt') {
      setCustomFields({
        ...customFields,
        questionField: 'content',
        answerField: 'content'
      });
    } else if (event.target.value === 'custom') {
      // 自定义格式保持当前值
    }
  };

  const handleSystemPromptChange = event => {
    setSystemPrompt(event.target.value);
  };

  const handleConfirmedOnlyChange = event => {
    setConfirmedOnly(event.target.checked);
  };

  // 新增处理函数
  const handleIncludeCOTChange = event => {
    setIncludeCOT(event.target.checked);
  };

  const handleCustomFieldChange = field => event => {
    setCustomFields({
      ...customFields,
      [field]: event.target.value
    });
  };

  const handleIncludeLabelsChange = event => {
    setCustomFields({
      ...customFields,
      includeLabels: event.target.checked
    });
  };

  const handleIncludeChunkChange = event => {
    setCustomFields({
      ...customFields,
      includeChunk: event.target.checked
    });
  };

  // 处理 Alpaca 字段类型变更
  const handleAlpacaFieldTypeChange = event => {
    setAlpacaFieldType(event.target.value);
  };

  // 处理自定义 instruction 变更
  const handleCustomInstructionChange = event => {
    setCustomInstruction(event.target.value);
  };

  const handleExport = () => {
    onExport({
      formatType,
      systemPrompt,
      confirmedOnly,
      fileFormat,
      includeCOT,
      alpacaFieldType, // 添加 alpaca 字段类型
      customInstruction, // 添加自定义 instruction
      customFields: formatType === 'custom' ? customFields : undefined
    });
  };

  return (
    <Dialog
      open={open}
      onClose={onClose}
      maxWidth="md"
      fullWidth
      PaperProps={{
        sx: {
          borderRadius: 2
        }
      }}
    >
      <DialogTitle>{t('export.title')}</DialogTitle>
      <DialogContent>
        <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
          <Tabs value={currentTab} onChange={(e, newValue) => setCurrentTab(newValue)} aria-label="export tabs">
            <Tab label={t('export.localTab')} />
            <Tab label={t('export.llamaFactoryTab')} />
            <Tab label={t('export.huggingFaceTab')} />
          </Tabs>
        </Box>

        {/* 第一个标签页：本地导出 */}
        {currentTab === 0 && (
          <LocalExportTab
            fileFormat={fileFormat}
            formatType={formatType}
            systemPrompt={systemPrompt}
            confirmedOnly={confirmedOnly}
            includeCOT={includeCOT}
            customFields={customFields}
            alpacaFieldType={alpacaFieldType}
            customInstruction={customInstruction}
            handleFileFormatChange={handleFileFormatChange}
            handleFormatChange={handleFormatChange}
            handleSystemPromptChange={handleSystemPromptChange}
            handleConfirmedOnlyChange={handleConfirmedOnlyChange}
            handleIncludeCOTChange={handleIncludeCOTChange}
            handleCustomFieldChange={handleCustomFieldChange}
            handleIncludeLabelsChange={handleIncludeLabelsChange}
            handleIncludeChunkChange={handleIncludeChunkChange}
            handleAlpacaFieldTypeChange={handleAlpacaFieldTypeChange}
            handleCustomInstructionChange={handleCustomInstructionChange}
            handleExport={handleExport}
          />
        )}

        {/* 第二个标签页：Llama Factory */}
        {currentTab === 1 && (
          <LlamaFactoryTab
            projectId={projectId}
            systemPrompt={systemPrompt}
            confirmedOnly={confirmedOnly}
            includeCOT={includeCOT}
            formatType={formatType}
            handleSystemPromptChange={handleSystemPromptChange}
            handleConfirmedOnlyChange={handleConfirmedOnlyChange}
            handleIncludeCOTChange={handleIncludeCOTChange}
          />
        )}

        {/* 第三个标签页：HuggingFace */}
        {currentTab === 2 && (
          <HuggingFaceTab
            projectId={projectId}
            systemPrompt={systemPrompt}
            confirmedOnly={confirmedOnly}
            includeCOT={includeCOT}
            formatType={formatType}
            fileFormat={fileFormat}
            customFields={customFields}
            handleSystemPromptChange={handleSystemPromptChange}
            handleConfirmedOnlyChange={handleConfirmedOnlyChange}
            handleIncludeCOTChange={handleIncludeCOTChange}
          />
        )}
      </DialogContent>
    </Dialog>
  );
};

export default ExportDatasetDialog;

'use client';

import { useState, useEffect } from 'react';
import { Container, Box, Typography, CircularProgress, Stack, useTheme } from '@mui/material';
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
import Navbar from '@/components/Navbar';
import HeroSection from '@/components/home/HeroSection';
import StatsCard from '@/components/home/StatsCard';
import ProjectList from '@/components/home/ProjectList';
import CreateProjectDialog from '@/components/home/CreateProjectDialog';
import MigrationDialog from '@/components/home/MigrationDialog';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';

export default function Home() {
  const { t } = useTranslation();
  const [projects, setProjects] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [createDialogOpen, setCreateDialogOpen] = useState(false);
  const [unmigratedProjects, setUnmigratedProjects] = useState([]);
  const [migrationDialogOpen, setMigrationDialogOpen] = useState(false);

  useEffect(() => {
    async function fetchProjects() {
      try {
        setLoading(true);
        // 获取用户创建的项目详情
        const response = await fetch(`/api/projects`);

        if (!response.ok) {
          throw new Error(t('projects.fetchFailed'));
        }

        const data = await response.json();
        setProjects(data);

        // 检查是否有未迁移的项目
        await checkUnmigratedProjects();
      } catch (error) {
        console.error(t('projects.fetchError'), String(error));
        setError(String(error));
      } finally {
        setLoading(false);
      }
    }

    // 检查未迁移的项目
    async function checkUnmigratedProjects() {
      try {
        const response = await fetch('/api/projects/unmigrated');

        if (!response.ok) {
          console.error('检查未迁移项目失败');
          return;
        }

        const { success, data } = await response.json();

        if (success && Array.isArray(data) && data.length > 0) {
          setUnmigratedProjects(data);
          setMigrationDialogOpen(true);
        }
      } catch (error) {
        console.error('检查未迁移项目出错', error);
      }
    }

    fetchProjects();
  }, []);

  const theme = useTheme();

  return (
    <main style={{ overflow: 'hidden', position: 'relative' }}>
      <Navbar projects={projects} />

      <HeroSection onCreateProject={() => setCreateDialogOpen(true)} />

      <Container
        maxWidth="lg"
        sx={{
          mt: { xs: 6, md: 8 },
          mb: { xs: 4, md: 6 },
          position: 'relative',
          zIndex: 1
        }}
      >
        {/* <StatsCard projects={projects} /> */}

        {loading && (
          <Box
            sx={{
              display: 'flex',
              justifyContent: 'center',
              width: '100%',
              mt: 6,
              flexDirection: 'column',
              alignItems: 'center',
              gap: 2
            }}
          >
            <CircularProgress size={40} thickness={4} />
            <Typography variant="body2" color="text.secondary">
              {t('projects.loading')}
            </Typography>
          </Box>
        )}

        {error && !loading && (
          <Box
            component={motion.div}
            initial={{ opacity: 0, scale: 0.95 }}
            animate={{ opacity: 1, scale: 1 }}
            transition={{ duration: 0.3 }}
            sx={{
              mt: 4,
              p: 3,
              bgcolor: 'error.light',
              borderRadius: 2,
              boxShadow: '0 4px 12px rgba(0,0,0,0.05)'
            }}
          >
            <Stack direction="row" spacing={1} alignItems="center">
              <ErrorOutlineIcon color="error" />
              <Typography color="error.dark">
                {t('projects.fetchFailed')}: {error}
              </Typography>
            </Stack>
          </Box>
        )}

        {!loading && (
          <motion.div
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ duration: 0.5, delay: 0.2 }}
          >
            <ProjectList projects={projects} onCreateProject={() => setCreateDialogOpen(true)} />
          </motion.div>
        )}
      </Container>

      <CreateProjectDialog open={createDialogOpen} onClose={() => setCreateDialogOpen(false)} />

      {/* 项目迁移对话框 */}
      <MigrationDialog
        open={migrationDialogOpen}
        onClose={() => setMigrationDialogOpen(false)}
        projectIds={unmigratedProjects}
      />
    </main>
  );
}

'use client';

import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  TextField,
  Button,
  Typography,
  Box,
  Alert,
  Paper,
  Divider
} from '@mui/material';

/**
 * 全自动蒸馏数据集配置弹框
 * @param {Object} props
 * @param {boolean} props.open - 对话框是否打开
 * @param {Function} props.onClose - 关闭对话框的回调
 * @param {Function} props.onStart - 开始蒸馏任务的回调
 * @param {string} props.projectId - 项目ID
 * @param {Object} props.project - 项目信息
 * @param {Object} props.stats - 当前统计信息
 */
export default function AutoDistillDialog({ open, onClose, onStart, projectId, project, stats = {} }) {
  const { t } = useTranslation();

  // 表单状态
  const [topic, setTopic] = useState('');
  const [levels, setLevels] = useState(2);
  const [tagsPerLevel, setTagsPerLevel] = useState(10);
  const [questionsPerTag, setQuestionsPerTag] = useState(10);

  // 计算信息
  const [estimatedTags, setEstimatedTags] = useState(0); // 所有标签总数（包括根节点和中间节点）
  const [leafTags, setLeafTags] = useState(0); // 叶子节点数量（即最后一层标签数）
  const [estimatedQuestions, setEstimatedQuestions] = useState(0);
  const [newTags, setNewTags] = useState(0);
  const [newQuestions, setNewQuestions] = useState(0);
  const [error, setError] = useState('');

  // 初始化默认主题
  useEffect(() => {
    if (project && project.name) {
      setTopic(project.name);
    }
  }, [project]);

  // 计算预估标签和问题数量
  useEffect(() => {
    /*
     * 根据公式：总问题数 = \left( \prod_{i=1}^{n} L_i \right) \times Q
     * 当每层标签数量相同(L)时：总问题数 = L^n \times Q
     */

    const leafTags = Math.pow(tagsPerLevel, levels);

    // 总问题数 = 叶子节点数 * 每个节点的问题数
    const totalQuestions = leafTags * questionsPerTag;

    let totalTags;
    if (tagsPerLevel === 1) {
      // 如果每层只有1个标签，总数就是 levels+1
      totalTags = levels + 1;
    } else {
      // 使用等比数列求和公式
      totalTags = (1 - Math.pow(tagsPerLevel, levels + 1)) / (1 - tagsPerLevel);
    }

    setLeafTags(leafTags);
    setEstimatedTags(leafTags); // 改为只显示叶子节点数量，而非所有节点数量
    setEstimatedQuestions(totalQuestions);

    // 计算新增标签和问题数量
    const currentTags = stats.tagsCount || 0;
    const currentQuestions = stats.questionsCount || 0;

    // 只考虑最后一层的标签数量
    setNewTags(Math.max(0, leafTags - currentTags));
    setNewQuestions(Math.max(0, totalQuestions - currentQuestions));

    // 验证是否可以执行任务
    if (leafTags <= currentTags && totalQuestions <= currentQuestions) {
      setError(t('distill.autoDistillInsufficientError'));
    } else {
      setError('');
    }
  }, [levels, tagsPerLevel, questionsPerTag, stats, t]);

  // 处理开始任务
  const handleStart = () => {
    if (error) return;

    onStart({
      topic,
      levels,
      tagsPerLevel,
      questionsPerTag,
      estimatedTags,
      estimatedQuestions
    });
  };

  return (
    <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
      <DialogTitle>{t('distill.autoDistillTitle')}</DialogTitle>
      <DialogContent>
        <Box sx={{ py: 2, display: 'flex', flexDirection: { xs: 'column', md: 'row' }, gap: 3 }}>
          {/* 左侧：输入区域 */}
          <Box sx={{ flex: 1 }}>
            <TextField
              label={t('distill.distillTopic')}
              value={topic}
              onChange={e => setTopic(e.target.value)}
              fullWidth
              margin="normal"
              required
              disabled
              helperText={t('distill.rootTopicHelperText')}
            />

            <Box sx={{ mt: 3, mb: 2 }}>
              <Typography gutterBottom>{t('distill.tagLevels')}</Typography>
              <TextField
                type="number"
                fullWidth
                InputProps={{
                  inputProps: { min: 1, max: 5 }
                }}
                value={levels}
                onChange={e => {
                  const value = Math.min(5, Math.max(1, Number(e.target.value)));
                  setLevels(value);
                }}
                helperText={t('distill.tagLevelsHelper', { max: 5 })}
              />
            </Box>

            <Box sx={{ mt: 3, mb: 2 }}>
              <Typography gutterBottom>{t('distill.tagsPerLevel')}</Typography>
              <TextField
                type="number"
                fullWidth
                InputProps={{
                  inputProps: { min: 1, max: 50 }
                }}
                value={tagsPerLevel}
                onChange={e => {
                  const value = Math.min(50, Math.max(1, Number(e.target.value)));
                  setTagsPerLevel(value);
                }}
                helperText={t('distill.tagsPerLevelHelper', { max: 50 })}
              />
            </Box>

            <Box sx={{ mt: 3, mb: 2 }}>
              <Typography gutterBottom>{t('distill.questionsPerTag')}</Typography>
              <TextField
                type="number"
                fullWidth
                InputProps={{
                  inputProps: { min: 1, max: 50 }
                }}
                value={questionsPerTag}
                onChange={e => {
                  const value = Math.min(50, Math.max(1, Number(e.target.value)));
                  setQuestionsPerTag(value);
                }}
                helperText={t('distill.questionsPerTagHelper', { max: 50 })}
              />
            </Box>
          </Box>

          {/* 右侧：预估信息区域 */}
          <Box sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
            <Paper
              variant="outlined"
              sx={{
                p: 3,
                mt: 1,
                borderRadius: 2,
                flex: 1,
                display: 'flex',
                flexDirection: 'column'
              }}
            >
              <Typography variant="h6" fontWeight="bold" gutterBottom>
                {t('distill.estimationInfo')}
              </Typography>

              <Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
                <Box>
                  <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2, mt: 2 }}>
                    <Typography variant="subtitle2">{t('distill.estimatedTags')}:</Typography>
                    <Typography variant="subtitle1" fontWeight="medium">
                      {estimatedTags}
                    </Typography>
                  </Box>

                  <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
                    <Typography variant="subtitle2">{t('distill.estimatedQuestions')}:</Typography>
                    <Typography variant="subtitle1" fontWeight="medium">
                      {estimatedQuestions}
                    </Typography>
                  </Box>

                  <Divider sx={{ my: 2 }} />

                  <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
                    <Typography variant="subtitle2">{t('distill.currentTags')}:</Typography>
                    <Typography variant="subtitle1" fontWeight="medium">
                      {stats.tagsCount || 0}
                    </Typography>
                  </Box>

                  <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
                    <Typography variant="subtitle2">{t('distill.currentQuestions')}:</Typography>
                    <Typography variant="subtitle1" fontWeight="medium">
                      {stats.questionsCount || 0}
                    </Typography>
                  </Box>
                </Box>

                <Box sx={{ pt: 2, borderTop: '1px dashed', borderColor: 'divider' }}>
                  <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
                    <Typography variant="subtitle1" color="primary">
                      {t('distill.newTags')}:
                    </Typography>
                    <Typography variant="h6" fontWeight="bold" color="primary.main">
                      {newTags}
                    </Typography>
                  </Box>

                  <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
                    <Typography variant="subtitle1" color="primary">
                      {t('distill.newQuestions')}:
                    </Typography>
                    <Typography variant="h6" fontWeight="bold" color="primary.main">
                      {newQuestions}
                    </Typography>
                  </Box>
                </Box>
              </Box>
            </Paper>

            {error && (
              <Alert severity="error" sx={{ mt: 2 }}>
                {error}
              </Alert>
            )}
          </Box>
        </Box>
      </DialogContent>
      <DialogActions>
        <Button onClick={onClose}>{t('common.cancel')}</Button>
        <Button onClick={handleStart} color="primary" variant="contained" disabled={!!error || !topic}>
          {t('distill.startAutoDistill')}
        </Button>
      </DialogActions>
    </Dialog>
  );
}

import { NextResponse } from 'next/server';
import { getQuestionsForTree, getQuestionsByTag } from '@/lib/db/questions';

/**
 * 获取项目的问题树形视图数据
 * @param {Request} request - 请求对象
 * @param {Object} params - 路由参数
 * @returns {Promise<Response>} - 包含问题数据的响应
 */
export async function GET(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: '项目ID不能为空' }, { status: 400 });
    }

    const { searchParams } = new URL(request.url);
    const tag = searchParams.get('tag');
    const input = searchParams.get('input');
    const tagsOnly = searchParams.get('tagsOnly') === 'true';
    const isDistill = searchParams.get('isDistill') === 'true';

    if (tag) {
      // 获取指定标签的问题数据（包含完整字段）
      const questions = await getQuestionsByTag(projectId, tag, input, isDistill);
      return NextResponse.json(questions);
    } else if (tagsOnly) {
      // 只获取标签信息（仅包含 id 和 label 字段）
      const treeData = await getQuestionsForTree(projectId, input, isDistill);
      return NextResponse.json(treeData);
    } else {
      // 兼容原有请求，获取树形视图数据（仅包含 id 和 label 字段）
      const treeData = await getQuestionsForTree(projectId, null, isDistill);
      return NextResponse.json(treeData);
    }
  } catch (error) {
    console.error('获取问题树形数据失败:', String(error));
    return NextResponse.json({ error: error.message || '获取问题树形数据失败' }, { status: 500 });
  }
}

'use server';
import { db } from '@/lib/db/index';
import { nanoid } from 'nanoid';

export async function getModelConfigByProjectId(projectId) {
  try {
    return await db.modelConfig.findMany({ where: { projectId } });
  } catch (error) {
    console.error('Failed to get modelConfig by projectId in database');
    throw error;
  }
}

export async function createInitModelConfig(data) {
  try {
    return await db.modelConfig.createManyAndReturn({ data });
  } catch (error) {
    console.error('Failed to create init modelConfig list in database');
    throw error;
  }
}

export async function getModelConfigById(id) {
  try {
    return await db.modelConfig.findUnique({ where: { id } });
  } catch (error) {
    console.error('Failed to get modelConfig by id in database');
    throw error;
  }
}

export async function deleteModelConfigById(id) {
  try {
    return await db.modelConfig.delete({ where: { id } });
  } catch (error) {
    console.error('Failed to delete modelConfig by id in database');
    throw error;
  }
}

export async function saveModelConfig(models) {
  try {
    if (!models.id) {
      models.id = nanoid(12);
    }
    return await db.modelConfig.upsert({ create: models, update: models, where: { id: models.id } });
  } catch (error) {
    console.error('Failed to create modelConfig in database');
    throw error;
  }
}

'use client';

import { Container, Box, Typography, Alert, Snackbar, Paper } from '@mui/material';
import ChunkViewDialog from '@/components/text-split/ChunkViewDialog';
import DatasetHeader from '@/components/datasets/DatasetHeader';
import DatasetMetadata from '@/components/datasets/DatasetMetadata';
import EditableField from '@/components/datasets/EditableField';
import OptimizeDialog from '@/components/datasets/OptimizeDialog';
import useDatasetDetails from '@/app/projects/[projectId]/datasets/[datasetId]/useDatasetDetails';
import { useTranslation } from 'react-i18next';

/**
 * 数据集详情页面
 */
export default function DatasetDetailsPage({ params }) {
  const { projectId, datasetId } = params;

  const { t } = useTranslation();
  // 使用自定义Hook管理状态和逻辑
  const {
    currentDataset,
    loading,
    editingAnswer,
    editingCot,
    answerValue,
    cotValue,
    snackbar,
    confirming,
    optimizeDialog,
    viewDialogOpen,
    viewChunk,
    datasetsAllCount,
    datasetsConfirmCount,
    answerTokens,
    cotTokens,
    shortcutsEnabled,
    setShortcutsEnabled,
    setSnackbar,
    setAnswerValue,
    setCotValue,
    setEditingAnswer,
    setEditingCot,
    handleNavigate,
    handleConfirm,
    handleSave,
    handleDelete,
    handleOpenOptimizeDialog,
    handleCloseOptimizeDialog,
    handleOptimize,
    handleViewChunk,
    handleCloseViewDialog
  } = useDatasetDetails(projectId, datasetId);

  // 加载状态
  if (loading) {
    return (
      <Container maxWidth="lg" sx={{ mt: 4 }}>
        <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '70vh' }}>
          <Alert severity="info">{t('datasets.loadingDataset')}</Alert>
        </Box>
      </Container>
    );
  }

  // 无数据状态
  if (!currentDataset) {
    return (
      <Container maxWidth="lg" sx={{ mt: 4 }}>
        <Alert severity="error">{t('datasets.datasetNotFound')}</Alert>
      </Container>
    );
  }

  return (
    <Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
      {/* 顶部导航栏 */}
      <DatasetHeader
        projectId={projectId}
        datasetsAllCount={datasetsAllCount}
        datasetsConfirmCount={datasetsConfirmCount}
        confirming={confirming}
        currentDataset={currentDataset}
        shortcutsEnabled={shortcutsEnabled}
        setShortcutsEnabled={setShortcutsEnabled}
        onNavigate={handleNavigate}
        onConfirm={handleConfirm}
        onDelete={handleDelete}
      />

      {/* 主要内容 */}
      <Paper sx={{ p: 3 }}>
        <Box sx={{ mb: 3 }}>
          <Typography variant="subtitle1" color="text.secondary" sx={{ mb: 1 }}>
            {t('datasets.question')}
          </Typography>
          <Typography variant="body1" sx={{ whiteSpace: 'pre-wrap' }}>
            {currentDataset.question}
          </Typography>
        </Box>

        <EditableField
          label={t('datasets.answer')}
          value={answerValue}
          editing={editingAnswer}
          onEdit={() => setEditingAnswer(true)}
          onChange={e => setAnswerValue(e.target.value)}
          onSave={() => handleSave('answer', answerValue)}
          onCancel={() => {
            setEditingAnswer(false);
            setAnswerValue(currentDataset.answer);
          }}
          onOptimize={handleOpenOptimizeDialog}
          tokenCount={answerTokens}
        />

        <EditableField
          label={t('datasets.cot')}
          value={cotValue}
          editing={editingCot}
          onEdit={() => setEditingCot(true)}
          onChange={e => setCotValue(e.target.value)}
          onSave={() => handleSave('cot', cotValue)}
          onCancel={() => {
            setEditingCot(false);
            setCotValue(currentDataset.cot || '');
          }}
          tokenCount={cotTokens}
        />

        <DatasetMetadata currentDataset={currentDataset} onViewChunk={handleViewChunk} />
      </Paper>

      {/* 消息提示 */}
      <Snackbar
        open={snackbar.open}
        autoHideDuration={2000}
        onClose={() => setSnackbar(prev => ({ ...prev, open: false }))}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
      >
        <Alert
          onClose={() => setSnackbar(prev => ({ ...prev, open: false }))}
          severity={snackbar.severity}
          sx={{ width: '100%' }}
        >
          {snackbar.message}
        </Alert>
      </Snackbar>

      {/* AI优化对话框 */}
      <OptimizeDialog
        open={optimizeDialog.open}
        onClose={handleCloseOptimizeDialog}
        onConfirm={handleOptimize}
        loading={optimizeDialog.loading}
      />

      {/* 文本块详情对话框 */}
      <ChunkViewDialog open={viewDialogOpen} chunk={viewChunk} onClose={handleCloseViewDialog} />
    </Container>
  );
}

'use client';

import { useState } from 'react';
import {
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  Button,
  TextField,
  RadioGroup,
  FormControlLabel,
  Radio,
  FormControl,
  FormLabel,
  Box,
  Typography,
  Alert,
  CircularProgress
} from '@mui/material';
import { useTranslation } from 'react-i18next';

/**
 * 批量编辑文本块对话框
 * @param {Object} props
 * @param {boolean} props.open - 对话框是否打开
 * @param {Function} props.onClose - 关闭对话框的回调
 * @param {Function} props.onConfirm - 确认编辑的回调
 * @param {Array} props.selectedChunks - 选中的文本块ID数组
 * @param {number} props.totalChunks - 文本块总数
 * @param {boolean} props.loading - 是否正在处理
 */
export default function BatchEditChunksDialog({
  open,
  onClose,
  onConfirm,
  selectedChunks = [],
  totalChunks = 0,
  loading = false
}) {
  const { t } = useTranslation();
  const [position, setPosition] = useState('start'); // 'start' 或 'end'
  const [content, setContent] = useState('');
  const [error, setError] = useState('');

  // 处理位置变更
  const handlePositionChange = event => {
    setPosition(event.target.value);
  };

  // 处理内容变更
  const handleContentChange = event => {
    setContent(event.target.value);
    if (error) setError('');
  };

  // 处理确认
  const handleConfirm = () => {
    if (!content.trim()) {
      setError(t('batchEdit.contentRequired'));
      return;
    }

    onConfirm({
      position,
      content: content.trim(),
      chunkIds: selectedChunks
    });
  };

  // 处理关闭
  const handleClose = () => {
    if (!loading) {
      setContent('');
      setError('');
      setPosition('start');
      onClose();
    }
  };

  return (
    <Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth disableEscapeKeyDown={loading}>
      <DialogTitle>{t('batchEdit.title')}</DialogTitle>

      <DialogContent>
        <Box sx={{ py: 1 }}>
          {/* 选择提示 */}
          <Alert severity="info" sx={{ mb: 3 }}>
            <Typography variant="body2">
              {selectedChunks.length === totalChunks
                ? t('batchEdit.allChunksSelected', { count: totalChunks })
                : t('batchEdit.selectedChunks', {
                    selected: selectedChunks.length,
                    total: totalChunks
                  })}
            </Typography>
          </Alert>

          {/* 位置选择 */}
          <FormControl component="fieldset" sx={{ mb: 3, width: '100%' }}>
            <FormLabel component="legend" sx={{ mb: 1 }}>
              {t('batchEdit.position')}
            </FormLabel>
            <RadioGroup value={position} onChange={handlePositionChange} row>
              <FormControlLabel value="start" control={<Radio />} label={t('batchEdit.atBeginning')} />
              <FormControlLabel value="end" control={<Radio />} label={t('batchEdit.atEnd')} />
            </RadioGroup>
          </FormControl>

          {/* 内容输入 */}
          <TextField
            fullWidth
            label={t('batchEdit.contentToAdd')}
            multiline
            rows={6}
            value={content}
            onChange={handleContentChange}
            placeholder={t('batchEdit.contentPlaceholder')}
            error={!!error}
            helperText={error || t('batchEdit.contentHelp')}
            disabled={loading}
            sx={{ mb: 2 }}
          />

          {/* 预览示例 */}
          {content.trim() && (
            <Box
              sx={{
                p: 2,
                border: '1px solid',
                borderColor: 'divider',
                borderRadius: 1,
                bgcolor: 'background.default',
                mb: 2
              }}
            >
              <Typography variant="subtitle2" sx={{ mb: 1 }}>
                {t('batchEdit.preview')}:
              </Typography>
              <Box
                sx={{
                  fontFamily: 'monospace',
                  fontSize: '0.875rem',
                  whiteSpace: 'pre-wrap',
                  color: 'text.secondary'
                }}
              >
                {position === 'start' ? (
                  <>
                    {content}
                    {'\n\n[原始文本块内容...]'}
                  </>
                ) : (
                  <>
                    {'[原始文本块内容...]\n\n'}
                    {content}
                  </>
                )}
              </Box>
            </Box>
          )}
        </Box>
      </DialogContent>

      <DialogActions>
        <Button onClick={handleClose} disabled={loading}>
          {t('common.cancel')}
        </Button>
        <Button
          onClick={handleConfirm}
          variant="contained"
          disabled={loading || !content.trim() || selectedChunks.length === 0}
          startIcon={loading ? <CircularProgress size={20} /> : null}
        >
          {loading ? t('batchEdit.processing') : t('batchEdit.applyToChunks', { count: selectedChunks.length })}
        </Button>
      </DialogActions>
    </Dialog>
  );
}

/**
 * Markdown文本分割工具主模块
 */

const parser = require('./core/parser');
const splitter = require('./core/splitter');
const summary = require('./core/summary');
const formatter = require('./output/formatter');
const fileWriter = require('./output/fileWriter');
const toc = require('./core/toc');

/**
 * 拆分Markdown文档
 * @param {string} markdownText - Markdown文本
 * @param {number} minSplitLength - 最小分割字数
 * @param {number} maxSplitLength - 最大分割字数
 * @returns {Array} - 分割结果数组
 */
function splitMarkdown(markdownText, minSplitLength, maxSplitLength) {
  // 解析文档结构
  const outline = parser.extractOutline(markdownText);

  // 按标题分割文档
  const sections = parser.splitByHeadings(markdownText, outline);

  // 处理段落，确保满足分割条件
  const res = splitter.processSections(sections, outline, minSplitLength, maxSplitLength);

  return res.map(r => ({
    result: `> **📑 Summarization：** *${r.summary}*\n\n---\n\n${r.content}`,
    ...r
  }));
}

// 导出模块功能
module.exports = {
  // 核心功能
  splitMarkdown,
  combineMarkdown: formatter.combineMarkdown,
  saveToSeparateFiles: fileWriter.saveToSeparateFiles,

  // 目录提取功能
  extractTableOfContents: toc.extractTableOfContents,
  tocToMarkdown: toc.tocToMarkdown,

  // 其他导出的子功能
  parser,
  splitter,
  summary,
  formatter,
  fileWriter,
  toc
};

/**
 * 摘要生成模块
 */

/**
 * 生成段落增强摘要，包含该段落中的所有标题
 * @param {Object} section - 段落对象
 * @param {Array} outline - 目录大纲
 * @param {number} partIndex - 子段落索引（可选）
 * @param {number} totalParts - 子段落总数（可选）
 * @returns {string} - 生成的增强摘要
 */
function generateEnhancedSummary(section, outline, partIndex = null, totalParts = null) {
  // 如果是文档前言
  if ((!section.heading && section.level === 0) || (!section.headings && !section.heading)) {
    // 获取文档标题（如果存在）
    const docTitle = outline.length > 0 && outline[0].level === 1 ? outline[0].title : '文档';
    return `${docTitle} 前言`;
  }

  // 如果有headings数组，使用它
  if (section.headings && section.headings.length > 0) {
    // 按照级别和位置排序标题
    const sortedHeadings = [...section.headings].sort((a, b) => {
      if (a.level !== b.level) return a.level - b.level;
      return a.position - b.position;
    });

    // 构建所有标题包含的摘要
    const headingsMap = new Map(); // 用于去重

    // 首先处理每个标题，找到其完整路径
    for (const heading of sortedHeadings) {
      // 跳过空标题
      if (!heading.heading) continue;

      // 查找当前标题在大纲中的位置
      const headingIndex = outline.findIndex(item => item.title === heading.heading && item.level === heading.level);

      if (headingIndex === -1) {
        // 如果在大纲中找不到，直接使用当前标题
        headingsMap.set(heading.heading, heading.heading);
        continue;
      }

      // 查找所有上级标题
      const pathParts = [];
      let parentLevel = heading.level - 1;

      for (let i = headingIndex - 1; i >= 0 && parentLevel > 0; i--) {
        if (outline[i].level === parentLevel) {
          pathParts.unshift(outline[i].title);
          parentLevel--;
        }
      }

      // 添加当前标题
      pathParts.push(heading.heading);

      // 生成完整路径并存储到Map中
      const fullPath = pathParts.join(' > ');
      headingsMap.set(fullPath, fullPath);
    }

    // 将所有标题路径转换为数组并按间隔符数量排序（表示层级深度）
    const paths = Array.from(headingsMap.values()).sort((a, b) => {
      const aDepth = (a.match(/>/g) || []).length;
      const bDepth = (b.match(/>/g) || []).length;
      return aDepth - bDepth || a.localeCompare(b);
    });

    // 如果没有有效的标题，返回默认摘要
    if (paths.length === 0) {
      return section.heading ? section.heading : '未命名段落';
    }

    // 如果是单个标题，直接返回
    if (paths.length === 1) {
      let summary = paths[0];
      // 如果是分段的部分，添加Part信息
      if (partIndex !== null && totalParts > 1) {
        summary += ` - Part ${partIndex}/${totalParts}`;
      }
      return summary;
    }

    // 如果有多个标题，生成多标题摘要
    let summary = '';

    // 尝试找到公共前缀
    const firstPath = paths[0];
    const segments = firstPath.split(' > ');

    for (let i = 0; i < segments.length - 1; i++) {
      const prefix = segments.slice(0, i + 1).join(' > ');
      let isCommonPrefix = true;

      for (let j = 1; j < paths.length; j++) {
        if (!paths[j].startsWith(prefix + ' > ')) {
          isCommonPrefix = false;
          break;
        }
      }

      if (isCommonPrefix) {
        summary = prefix + ' > [';
        // 添加非公共部分
        for (let j = 0; j < paths.length; j++) {
          const uniquePart = paths[j].substring(prefix.length + 3); // +3 为 ' > ' 的长度
          summary += (j > 0 ? ', ' : '') + uniquePart;
        }
        summary += ']';
        break;
      }
    }

    // 如果没有公共前缀，使用完整列表
    if (!summary) {
      summary = paths.join(', ');
    }

    // 如果是分段的部分，添加Part信息
    if (partIndex !== null && totalParts > 1) {
      summary += ` - Part ${partIndex}/${totalParts}`;
    }

    return summary;
  }

  // 兼容旧逻辑，当没有headings数组时
  if (!section.heading && section.level === 0) {
    return '文档前言';
  }

  // 查找当前段落在大纲中的位置
  const currentHeadingIndex = outline.findIndex(item => item.title === section.heading && item.level === section.level);

  if (currentHeadingIndex === -1) {
    return section.heading ? section.heading : '未命名段落';
  }

  // 查找所有上级标题
  const parentHeadings = [];
  let parentLevel = section.level - 1;

  for (let i = currentHeadingIndex - 1; i >= 0 && parentLevel > 0; i--) {
    if (outline[i].level === parentLevel) {
      parentHeadings.unshift(outline[i].title);
      parentLevel--;
    }
  }

  // 构建摘要
  let summary = '';

  if (parentHeadings.length > 0) {
    summary = parentHeadings.join(' > ') + ' > ';
  }

  summary += section.heading;

  // 如果是分段的部分，添加Part信息
  if (partIndex !== null && totalParts > 1) {
    summary += ` - Part ${partIndex}/${totalParts}`;
  }

  return summary;
}

/**
 * 旧的摘要生成函数，保留供兼容性使用
 * @param {Object} section - 段落对象
 * @param {Array} outline - 目录大纲
 * @param {number} partIndex - 子段落索引（可选）
 * @param {number} totalParts - 子段落总数（可选）
 * @returns {string} - 生成的摘要
 */
function generateSummary(section, outline, partIndex = null, totalParts = null) {
  return generateEnhancedSummary(section, outline, partIndex, totalParts);
}

module.exports = {
  generateEnhancedSummary,
  generateSummary
};

import { NextResponse } from 'next/server';
import { getDatasetsById, getDatasetsCounts, getNavigationItems } from '@/lib/db/datasets';

/**
 * 获取项目的所有数据集
 */
export async function GET(request, { params }) {
  try {
    const { projectId, datasetId } = params;
    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: '项目ID不能为空' }, { status: 400 });
    }
    if (!datasetId) {
      return NextResponse.json({ error: '数据集ID不能为空' }, { status: 400 });
    }
    const { searchParams } = new URL(request.url);
    const operateType = searchParams.get('operateType');
    if (operateType !== null) {
      const data = await getNavigationItems(projectId, datasetId, operateType);
      return NextResponse.json(data);
    }
    const datasets = await getDatasetsById(datasetId);
    let counts = await getDatasetsCounts(projectId);

    return NextResponse.json({ datasets, ...counts });
  } catch (error) {
    console.error('获取数据集详情失败:', String(error));
    return NextResponse.json(
      {
        error: error.message || '获取数据集详情失败'
      },
      { status: 500 }
    );
  }
}

'use client';

import { useEffect, useRef } from 'react';
import { useTheme } from '@mui/material';

export default function ParticleBackground() {
  const canvasRef = useRef(null);
  const theme = useTheme();

  useEffect(() => {
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    let animationFrameId;
    let particles = [];
    let mousePosition = { x: 0, y: 0 };
    let hoverRadius = 150; // 增加鼠标影响范围
    let mouseSpeed = { x: 0, y: 0 }; // 跟踪鼠标速度
    let lastMousePosition = { x: 0, y: 0 }; // 上一帧鼠标位置

    // 设置画布大小为窗口大小
    const handleResize = () => {
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      initParticles();
    };

    // 跟踪鼠标位置和速度
    const handleMouseMove = event => {
      // 计算鼠标速度
      mouseSpeed.x = event.clientX - mousePosition.x;
      mouseSpeed.y = event.clientY - mousePosition.y;

      // 更新鼠标位置
      lastMousePosition.x = mousePosition.x;
      lastMousePosition.y = mousePosition.y;
      mousePosition.x = event.clientX;
      mousePosition.y = event.clientY;
    };

    // 触摸设备支持
    const handleTouchMove = event => {
      if (event.touches.length > 0) {
        // 计算触摸速度
        mouseSpeed.x = event.touches[0].clientX - mousePosition.x;
        mouseSpeed.y = event.touches[0].clientY - mousePosition.y;

        // 更新触摸位置
        lastMousePosition.x = mousePosition.x;
        lastMousePosition.y = mousePosition.y;
        mousePosition.x = event.touches[0].clientX;
        mousePosition.y = event.touches[0].clientY;
      }
    };

    // 生成随机颜色
    const getRandomColor = () => {
      // 主题色调
      const colors =
        theme.palette.mode === 'dark'
          ? [
              'rgba(255, 255, 255, 0.5)', // 白色
              'rgba(100, 181, 246, 0.5)', // 蓝色
              'rgba(156, 39, 176, 0.4)', // 紫色
              'rgba(121, 134, 203, 0.5)' // 靛蓝色
            ]
          : [
              'rgba(42, 92, 170, 0.5)', // 主蓝色
              'rgba(66, 165, 245, 0.4)', // 浅蓝色
              'rgba(94, 53, 177, 0.3)', // 深紫色
              'rgba(3, 169, 244, 0.4)' // 天蓝色
            ];

      return colors[Math.floor(Math.random() * colors.length)];
    };

    // 初始化粒子
    const initParticles = () => {
      particles = [];
      // 增加粒子数量，但保持性能平衡
      const particleCount = Math.min(Math.floor(window.innerWidth / 8), 150);

      for (let i = 0; i < particleCount; i++) {
        // 创建不同大小和速度的粒子
        const size = Math.random();
        const speedFactor = Math.max(0.1, size); // 较大的粒子移动较慢

        particles.push({
          x: Math.random() * canvas.width,
          y: Math.random() * canvas.height,
          // 粒子大小更加多样化
          radius: size * 3 + 0.5,
          // 使用随机颜色
          color: getRandomColor(),
          // 添加发光效果
          glow: Math.random() * 10 + 5,
          // 调整速度范围，使运动更加自然
          speedX: (Math.random() * 0.6 - 0.3) * speedFactor,
          speedY: (Math.random() * 0.6 - 0.3) * speedFactor,
          originalSpeedX: (Math.random() * 0.6 - 0.3) * speedFactor,
          originalSpeedY: (Math.random() * 0.6 - 0.3) * speedFactor,
          // 添加脉动效果
          pulseSpeed: Math.random() * 0.02 + 0.01,
          pulseDirection: Math.random() > 0.5 ? 1 : -1,
          pulseAmount: 0,
          // 粒子透明度
          opacity: Math.random() * 0.5 + 0.5
        });
      }
    };

    // 绘制粒子
    const drawParticles = () => {
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      // 计算鼠标速度衰减
      mouseSpeed.x *= 0.95;
      mouseSpeed.y *= 0.95;

      // 绘制粒子之间的连线
      drawLines();

      particles.forEach(particle => {
        // 计算粒子与鼠标的距离
        const dx = mousePosition.x - particle.x;
        const dy = mousePosition.y - particle.y;
        const distance = Math.sqrt(dx * dx + dy * dy);

        // 脉动效果
        particle.pulseAmount += particle.pulseSpeed * particle.pulseDirection;
        if (Math.abs(particle.pulseAmount) > 0.5) {
          particle.pulseDirection *= -1;
        }

        // 如果粒子在鼠标影响范围内，调整其速度
        if (distance < hoverRadius) {
          const angle = Math.atan2(dy, dx);
          const force = (hoverRadius - distance) / hoverRadius;
          const mouseFactor = 3; // 增强鼠标影响力度

          // 粒子远离鼠标，并受鼠标速度影响
          particle.speedX = -Math.cos(angle) * force * mouseFactor + particle.originalSpeedX + mouseSpeed.x * 0.05;
          particle.speedY = -Math.sin(angle) * force * mouseFactor + particle.originalSpeedY + mouseSpeed.y * 0.05;
        } else {
          // 逐渐恢复原始速度
          particle.speedX = particle.speedX * 0.95 + particle.originalSpeedX * 0.05;
          particle.speedY = particle.speedY * 0.95 + particle.originalSpeedY * 0.05;
        }

        // 更新粒子位置
        particle.x += particle.speedX;
        particle.y += particle.speedY;

        // 边界检查
        if (particle.x < 0) particle.x = canvas.width;
        if (particle.x > canvas.width) particle.x = 0;
        if (particle.y < 0) particle.y = canvas.height;
        if (particle.y > canvas.height) particle.y = 0;

        // 应用脉动效果到粒子大小
        const currentRadius = particle.radius * (1 + particle.pulseAmount * 0.2);

        // 绘制发光效果
        const gradient = ctx.createRadialGradient(particle.x, particle.y, 0, particle.x, particle.y, particle.glow);
        gradient.addColorStop(0, particle.color);
        gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');

        // 绘制粒子
        ctx.beginPath();
        ctx.arc(particle.x, particle.y, currentRadius, 0, Math.PI * 2);
        ctx.fillStyle = particle.color;
        ctx.fill();

        // 添加发光效果
        ctx.beginPath();
        ctx.arc(particle.x, particle.y, particle.glow, 0, Math.PI * 2);
        ctx.fillStyle = gradient;
        ctx.globalAlpha = 0.3 * particle.opacity;
        ctx.fill();
        ctx.globalAlpha = 1.0;
      });

      animationFrameId = requestAnimationFrame(drawParticles);
    };

    // 绘制粒子之间的连线
    const drawLines = () => {
      for (let i = 0; i < particles.length; i++) {
        for (let j = i + 1; j < particles.length; j++) {
          const dx = particles[i].x - particles[j].x;
          const dy = particles[i].y - particles[j].y;
          const distance = Math.sqrt(dx * dx + dy * dy);

          // 增加连线的最大距离
          const maxDistance = 120;

          if (distance < maxDistance) {
            // 只在粒子距离小于maxDistance时绘制连线
            ctx.beginPath();
            ctx.moveTo(particles[i].x, particles[i].y);
            ctx.lineTo(particles[j].x, particles[j].y);

            // 根据距离设置线条透明度
            const opacity = 1 - distance / maxDistance;

            // 根据主题设置线条颜色
            const lineColor =
              theme.palette.mode === 'dark'
                ? `rgba(255, 255, 255, ${opacity * 0.2})`
                : `rgba(42, 92, 170, ${opacity * 0.2})`;

            ctx.strokeStyle = lineColor;
            ctx.lineWidth = opacity * 1.5; // 根据距离调整线宽
            ctx.stroke();
          }
        }
      }
    };

    // 初始化
    handleResize();
    window.addEventListener('resize', handleResize);
    window.addEventListener('mousemove', handleMouseMove);
    window.addEventListener('touchmove', handleTouchMove);

    // 开始动画
    drawParticles();

    // 清理函数
    return () => {
      window.removeEventListener('resize', handleResize);
      window.removeEventListener('mousemove', handleMouseMove);
      window.removeEventListener('touchmove', handleTouchMove);
      cancelAnimationFrame(animationFrameId);
    };
  }, [theme.palette.mode]);

  return (
    <canvas
      ref={canvasRef}
      style={{
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%',
        height: '100%',
        pointerEvents: 'none', // 确保不会干扰下方元素的交互
        zIndex: 0
      }}
    />
  );
}

import { NextResponse } from 'next/server';
import { createLlmModels, getLlmModelsByProviderId } from '@/lib/db/llm-models'; // 导入db实例

// 获取LLM模型
export async function GET(request) {
  try {
    const searchParams = request.nextUrl.searchParams;
    let providerId = searchParams.get('providerId');
    if (!providerId) {
      return NextResponse.json({ error: '参数错误' }, { status: 400 });
    }
    const models = await getLlmModelsByProviderId(providerId);
    if (!models) {
      return NextResponse.json({ error: 'LLM provider not found' }, { status: 404 });
    }
    return NextResponse.json(models);
  } catch (error) {
    console.error('Database query error:', String(error));
    return NextResponse.json({ error: 'Database query failed' }, { status: 500 });
  }
}

//同步最新模型列表
export async function POST(request) {
  try {
    const { newModels, providerId } = await request.json();
    const models = await getLlmModelsByProviderId(providerId);
    const existingModelIds = models.map(model => model.modelId);
    const diffModels = newModels.filter(item => !existingModelIds.includes(item.modelId));
    if (diffModels.length > 0) {
      // return NextResponse.json(await createLlmModels(diffModels));
      return NextResponse.json({ message: 'No new models to insert' }, { status: 200 });
    } else {
      return NextResponse.json({ message: 'No new models to insert' }, { status: 200 });
    }
  } catch (error) {
    return NextResponse.json({ error: 'Database insert failed' }, { status: 500 });
  }
}

// LlamaFactoryTab.js 组件
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
  Button,
  FormControlLabel,
  Checkbox,
  Typography,
  Box,
  TextField,
  Alert,
  CircularProgress,
  IconButton,
  Tooltip
} from '@mui/material';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import CheckIcon from '@mui/icons-material/Check';

const LlamaFactoryTab = ({
  projectId,
  systemPrompt,
  confirmedOnly,
  includeCOT,
  formatType,
  handleSystemPromptChange,
  handleConfirmedOnlyChange,
  handleIncludeCOTChange
}) => {
  const { t } = useTranslation();
  const [configExists, setConfigExists] = useState(false);
  const [configPath, setConfigPath] = useState('');
  const [generating, setGenerating] = useState(false);
  const [error, setError] = useState('');
  const [copied, setCopied] = useState(false);

  // 检查配置文件是否存在
  useEffect(() => {
    if (projectId) {
      fetch(`/api/projects/${projectId}/llamaFactory/checkConfig`)
        .then(res => res.json())
        .then(data => {
          setConfigExists(data.exists);
          if (data.exists) {
            setConfigPath(data.configPath);
          }
        })
        .catch(err => {
          setError(err.message);
        });
    }
  }, [projectId, configExists]);

  // 复制路径到剪贴板
  const handleCopyPath = () => {
    const path = configPath.replace('dataset_info.json', '');
    navigator.clipboard.writeText(path).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  };

  // 处理生成 Llama Factory 配置
  const handleGenerateConfig = async () => {
    try {
      setGenerating(true);
      setError('');

      const response = await fetch(`/api/projects/${projectId}/llamaFactory/generate`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          formatType,
          systemPrompt,
          confirmedOnly,
          includeCOT
        })
      });

      const data = await response.json();
      if (!response.ok) {
        throw new Error(data.error);
      }

      setConfigExists(true);
    } catch (err) {
      setError(err.message);
    } finally {
      setGenerating(false);
    }
  };

  return (
    <Box sx={{ mt: 2 }}>
      {error && (
        <Alert severity="error" sx={{ mt: 2 }}>
          {error}
        </Alert>
      )}

      <Box sx={{ mb: 3 }}>
        <Typography variant="subtitle1" gutterBottom>
          {t('export.systemPrompt')}
        </Typography>
        <TextField
          fullWidth
          multiline
          rows={3}
          value={systemPrompt}
          onChange={handleSystemPromptChange}
          variant="outlined"
        />
      </Box>

      <Box sx={{ mb: 2, display: 'flex', flexDirection: 'row', gap: 4 }}>
        <FormControlLabel
          control={<Checkbox checked={confirmedOnly} onChange={handleConfirmedOnlyChange} />}
          label={t('export.onlyConfirmed')}
        />

        <FormControlLabel
          control={<Checkbox checked={includeCOT} onChange={handleIncludeCOTChange} />}
          label={t('export.includeCOT')}
        />
      </Box>

      {configExists ? (
        <>
          <Alert severity="success" sx={{ mb: 2 }}>
            {t('export.configExists')}
          </Alert>
          <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
            <Typography variant="body2" color="text.secondary">
              {t('export.configPath')}: {configPath.replace('dataset_info.json', '')}
            </Typography>
            <Tooltip title={copied ? t('common.copied') : t('common.copy')}>
              <IconButton size="small" onClick={handleCopyPath} sx={{ ml: 1 }}>
                {copied ? <CheckIcon fontSize="small" color="success" /> : <ContentCopyIcon fontSize="small" />}
              </IconButton>
            </Tooltip>
          </Box>
        </>
      ) : (
        <Typography variant="body2" color="text.secondary" gutterBottom>
          {t('export.noConfig')}
        </Typography>
      )}

      <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}>
        <Button onClick={handleGenerateConfig} variant="contained" disabled={generating} sx={{ borderRadius: 2 }}>
          {generating ? (
            <CircularProgress size={24} />
          ) : configExists ? (
            t('export.updateConfig')
          ) : (
            t('export.generateConfig')
          )}
        </Button>
      </Box>
    </Box>
  );
};

export default LlamaFactoryTab;

/**
 * LLM API 统一调用工具类
 * 支持多种模型提供商：OpenAI、Ollama、智谱AI等
 * 支持普通输出和流式输出
 */
import { DEFAULT_MODEL_SETTINGS } from '@/constant/model';
import { extractThinkChain, extractAnswer } from '@/lib/llm/common/util';
const OllamaClient = require('./providers/ollama'); // 导入 OllamaClient
const OpenAIClient = require('./providers/openai'); // 导入 OpenAIClient
const ZhiPuClient = require('./providers/zhipu'); // 导入 ZhiPuClient
const OpenRouterClient = require('./providers/openrouter');

class LLMClient {
  /**
   * 创建 LLM 客户端实例
   * @param {Object} config - 配置信息
   * @param {string} config.provider - 提供商名称，如 'openai', 'ollama', 'zhipu' 等
   * @param {string} config.endpoint - API 端点，如 'https://api.openai.com/v1/'
   * @param {string} config.apiKey - API 密钥（如果需要）
   * @param {string} config.model - 模型名称，如 'gpt-3.5-turbo', 'llama2' 等
   * @param {number} config.temperature - 温度参数
   */
  constructor(config = {}) {
    this.config = {
      provider: config.providerId || 'openai',
      endpoint: this._handleEndpoint(config.providerId, config.endpoint) || '',
      apiKey: config.apiKey || '',
      model: config.modelName || '',
      temperature: config.temperature || DEFAULT_MODEL_SETTINGS.temperature,
      maxTokens: config.maxTokens || DEFAULT_MODEL_SETTINGS.maxTokens,
      max_tokens: config.maxTokens || DEFAULT_MODEL_SETTINGS.maxTokens
    };
    if (config.topP !== 0) {
      this.config.topP = config.topP;
    }
    if (config.topK !== 0) {
      this.config.topK = config.topK;
    }

    this.client = this._createClient(this.config.provider, this.config);
  }

  /**
   * 兼容之前版本的用户配置
   */
  _handleEndpoint(provider, endpoint) {
    if (provider.toLowerCase() === 'ollama') {
      if (endpoint.endsWith('v1/') || endpoint.endsWith('v1')) {
        return endpoint.replace('v1', 'api');
      }
    }
    if (endpoint.includes('/chat/completions')) {
      return endpoint.replace('/chat/completions', '');
    }
    return endpoint;
  }

  _createClient(provider, config) {
    const clientMap = {
      ollama: OllamaClient,
      openai: OpenAIClient,
      siliconflow: OpenAIClient,
      deepseek: OpenAIClient,
      zhipu: ZhiPuClient,
      openrouter: OpenRouterClient
    };
    const ClientClass = clientMap[provider.toLowerCase()] || OpenAIClient;
    return new ClientClass(config);
  }

  async _callClientMethod(method, ...args) {
    try {
      return await this.client[method](...args);
    } catch (error) {
      console.error(`${this.config.provider} API 调用出错:`, error);
      throw error;
    }
  }

  /**
   * 生成对话响应
   * @param {string|Array} prompt - 用户输入的提示词或对话历史
   * @param {Object} options - 可选参数
   * @returns {Promise<Object>} 返回模型响应
   */
  async chat(prompt, options = {}) {
    const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }];
    options = {
      ...options,
      ...this.config
    };
    return this._callClientMethod('chat', messages, options);
  }

  /**
   * 流式生成对话响应
   * @param {string|Array} prompt - 用户输入的提示词或对话历史
   * @param {Object} options - 可选参数
   * @returns {ReadableStream} 返回可读流
   */
  /**
   * 纯API流式生成对话响应
   * @param {string|Array} prompt - 用户输入的提示词或对话历史
   * @param {Object} options - 可选参数
   * @returns {Response} 返回原生Response对象
   */
  async chatStreamAPI(prompt, options = {}) {
    const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }];
    options = {
      ...options,
      ...this.config
    };
    return this._callClientMethod('chatStreamAPI', messages, options);
  }

  /**
   * 流式生成对话响应
   * @param {string|Array} prompt - 用户输入的提示词或对话历史
   * @param {Object} options - 可选参数
   * @returns {ReadableStream} 返回可读流
   */
  async chatStream(prompt, options = {}) {
    const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }];
    options = {
      ...options,
      ...this.config
    };
    return this._callClientMethod('chatStream', messages, options);
  }

  // 获取模型响应
  async getResponse(prompt, options = {}) {
    const llmRes = await this.chat(prompt, options);
    return llmRes.text || llmRes.response.messages || '';
  }

  async getResponseWithCOT(prompt, options = {}) {
    const llmRes = await this.chat(prompt, options);
    let answer = llmRes.text || '';
    let cot = llmRes.reasoning || '';
    if ((answer && answer.startsWith('<think>')) || answer.startsWith('<thinking>')) {
      cot = extractThinkChain(answer);
      answer = extractAnswer(answer);
    } else if (
      llmRes?.response?.body?.choices?.length > 0 &&
      llmRes.response.body.choices[0].message.reasoning_content
    ) {
      if (llmRes.response.body.choices[0].message.reasoning_content) {
        cot = llmRes.response.body.choices[0].message.reasoning_content;
      }
      if (llmRes.response.body.choices[0].message.content) {
        answer = llmRes.response.body.choices[0].message.content;
      }
    }
    if (answer.startsWith('\n\n')) {
      answer = answer.slice(2);
    }
    if (cot.endsWith('\n\n')) {
      cot = cot.slice(0, -2);
    }
    return { answer, cot };
  }
}

module.exports = LLMClient;

import React, { useState } from 'react';
import { Box, Paper, Typography, Alert, useTheme, IconButton, Collapse } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import PsychologyIcon from '@mui/icons-material/Psychology';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import { useTranslation } from 'react-i18next';

/**
 * 聊天消息组件
 * @param {Object} props
 * @param {Object} props.message - 消息对象
 * @param {string} props.message.role - 消息角色：'user'、'assistant' 或 'error'
 * @param {string} props.message.content - 消息内容
 * @param {string} props.modelName - 模型名称（仅在 assistant 或 error 类型消息中显示）
 */
export default function ChatMessage({ message, modelName }) {
  const theme = useTheme();
  const { t } = useTranslation();

  // 用户消息
  if (message.role === 'user') {
    return (
      <Box
        sx={{
          display: 'flex',
          justifyContent: 'flex-end',
          mb: 2
        }}
      >
        <Paper
          elevation={1}
          sx={{
            p: 2,
            borderRadius: '16px 16px 0 16px',
            maxWidth: '80%',
            bgcolor: theme.palette.primary.main,
            color: 'white'
          }}
        >
          {typeof message.content === 'string' ? (
            <Typography variant="body1">{message.content}</Typography>
          ) : (
            // 如果是数组类型（用于视觉模型的用户输入）
            <>
              {Array.isArray(message.content) &&
                message.content.map((item, i) => {
                  if (item.type === 'text') {
                    return (
                      <Typography key={i} variant="body1">
                        {item.text}
                      </Typography>
                    );
                  } else if (item.type === 'image_url') {
                    return (
                      <Box key={i} sx={{ mt: 1, mb: 1 }}>

                      </Box>
                    );
                  }
                  return null;
                })}
            </>
          )}
        </Paper>
      </Box>
    );
  }

  // 助手消息
  if (message.role === 'assistant') {
    // 处理推理过程的展示状态
    const [showThinking, setShowThinking] = useState(message.showThinking || false);
    const hasThinking = message.thinking && message.thinking.trim().length > 0;

    return (
      <Box
        sx={{
          display: 'flex',
          justifyContent: 'flex-start',
          mb: 2
        }}
      >
        <Paper
          elevation={1}
          sx={{
            p: 2,
            borderRadius: '16px 16px 16px 0',
            maxWidth: '80%',
            width: hasThinking ? '80%' : 'auto',
            bgcolor: theme.palette.mode === 'dark' ? theme.palette.grey[800] : theme.palette.grey[100]
          }}
        >
          {modelName && (
            <Typography variant="caption" color="textSecondary" sx={{ display: 'block', mb: 0.5 }}>
              {modelName}
            </Typography>
          )}

          {/* 推理过程显示区域 */}
          {hasThinking && (
            <Box sx={{ mb: 2 }}>
              <Box
                sx={{
                  display: 'flex',
                  justifyContent: 'space-between',
                  alignItems: 'center',
                  mb: 1,
                  borderBottom: `1px solid ${theme.palette.divider}`
                }}
              >
                <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
                  {message.isStreaming ? (
                    <AutoFixHighIcon
                      fontSize="small"
                      color="primary"
                      sx={{
                        animation: 'thinking-pulse 1.5s infinite',
                        '@keyframes thinking-pulse': {
                          '0%': { opacity: 0.4 },
                          '50%': { opacity: 1 },
                          '100%': { opacity: 0.4 }
                        }
                      }}
                    />
                  ) : (
                    <PsychologyIcon fontSize="small" color="primary" />
                  )}
                  <Typography variant="caption" color="primary" fontWeight="bold">
                    {t('playground.reasoningProcess', '推理过程')}
                  </Typography>
                </Box>
                <IconButton size="small" onClick={() => setShowThinking(!showThinking)} sx={{ p: 0 }}>
                  {showThinking ? <ExpandLessIcon fontSize="small" /> : <ExpandMoreIcon fontSize="small" />}
                </IconButton>
              </Box>

              <Collapse in={showThinking}>
                <Box
                  sx={{
                    p: 1,
                    bgcolor: theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.05)',
                    borderRadius: 1,
                    fontFamily: 'monospace',
                    fontSize: '0.85rem'
                  }}
                >
                  <Typography variant="body2" sx={{ whiteSpace: 'pre-wrap', color: theme.palette.text.secondary }}>
                    {message.thinking}
                  </Typography>
                </Box>
              </Collapse>
            </Box>
          )}

          {/* 回答内容 */}
          <Typography variant="body1" sx={{ whiteSpace: 'pre-wrap' }}>
            {typeof message.content === 'string' ? (
              <>
                {message.content}
                {message.isStreaming && |}
              </>
            ) : (
              // 如果是数组类型（用于视觉模型的响应）
              <>
                {Array.isArray(message.content) &&
                  message.content.map((item, i) => {
                    if (item.type === 'text') {
                      return {item.text};
                    } else if (item.type === 'image_url') {
                      return (
                        <Box key={i} sx={{ mt: 1, mb: 1 }}>

                        </Box>
                      );
                    }
                    return null;
                  })}
                {message.isStreaming && |}
              </>
            )}
          </Typography>
        </Paper>
      </Box>
    );
  }

  // 错误消息
  if (message.role === 'error') {
    return (
      <Box
        sx={{
          display: 'flex',
          justifyContent: 'flex-start',
          mb: 2
        }}
      >
        <Alert severity="error" sx={{ maxWidth: '80%' }}>
          {modelName && (
            <Typography variant="caption" sx={{ display: 'block', mb: 0.5 }}>
              {modelName}
            </Typography>
          )}
          {message.content}
        </Alert>
      </Box>
    );
  }

  return null;
}

/**
 * 文件输出模块
 */

const fs = require('fs');
const path = require('path');
const { ensureDirectoryExists } = require('../utils/common');

/**
 * 将分割结果保存到单独的文件
 * @param {Array} splitResult - 分割结果数组
 * @param {string} baseFilename - 基础文件名（不包含扩展名）
 * @param {Function} callback - 回调函数
 */
function saveToSeparateFiles(splitResult, baseFilename, callback) {
  // 获取基础目录和文件名（无扩展名）
  const basePath = path.dirname(baseFilename);
  const filenameWithoutExt = path.basename(baseFilename).replace(/\.[^/.]+$/, '');

  // 创建用于存放分割文件的目录
  const outputDir = path.join(basePath, `${filenameWithoutExt}_parts`);

  // 确保目录存在
  ensureDirectoryExists(outputDir);

  // 递归保存文件
  function saveFile(index) {
    if (index >= splitResult.length) {
      // 所有文件保存完成
      callback(null, outputDir, splitResult.length);
      return;
    }

    const part = splitResult[index];
    const paddedIndex = String(index + 1).padStart(3, '0'); // 确保文件排序正确
    const outputFile = path.join(outputDir, `${filenameWithoutExt}_part${paddedIndex}.md`);

    // 将摘要和内容格式化为Markdown
    const content = `> **📑 Summarization：** *${part.summary}*\n\n---\n\n${part.content}`;

    fs.writeFile(outputFile, content, 'utf8', err => {
      if (err) {
        callback(err);
        return;
      }

      // 继续保存下一个文件
      saveFile(index + 1);
    });
  }

  // 开始保存文件
  saveFile(0);
}

module.exports = {
  saveToSeparateFiles
};

import React from 'react';
import {
  FormControl,
  InputLabel,
  Select,
  MenuItem,
  OutlinedInput,
  Box,
  Chip,
  Checkbox,
  ListItemText
} from '@mui/material';
import { useTranslation } from 'react-i18next';

const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
  PaperProps: {
    style: {
      maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
      width: 250
    }
  }
};

/**
 * 模型选择组件
 * @param {Object} props
 * @param {Array} props.models - 可用模型列表
 * @param {Array} props.selectedModels - 已选择的模型ID列表
 * @param {Function} props.onChange - 选择改变时的回调函数
 */
export default function ModelSelector({ models, selectedModels, onChange }) {
  // 获取模型名称
  const getModelName = modelId => {
    const model = models.find(m => m.id === modelId);
    return model ? `${model.providerName}: ${model.modelName}` : modelId;
  };
  const { t } = useTranslation();

  return (
    <FormControl fullWidth>
      <InputLabel id="model-select-label">{t('playground.selectModelMax3')}</InputLabel>
      <Select
        labelId="model-select-label"
        id="model-select"
        multiple
        value={selectedModels}
        onChange={onChange}
        input={<OutlinedInput label="选择模型（最多3个）" />}
        renderValue={selected => (
          <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
            {selected.map(modelId => (
              <Chip key={modelId} label={getModelName(modelId)} color="primary" variant="outlined" size="small" />
            ))}
          </Box>
        )}
        MenuProps={MenuProps}
      >
        {models
          .filter(m => {
            if (m.providerId.toLowerCase() === 'ollama') {
              return m.modelName && m.endpoint;
            } else {
              return m.modelName && m.endpoint && m.apiKey;
            }
          })
          .map(model => (
            <MenuItem
              key={model.id}
              value={model.id}
              disabled={selectedModels.length >= 3 && !selectedModels.includes(model.id)}
            >
              <Checkbox checked={selectedModels.indexOf(model.id) > -1} />
              <ListItemText primary={`${model.providerName}: ${model.modelName}`} />
            </MenuItem>
          ))}
      </Select>
    </FormControl>
  );
}

import { NextResponse } from 'next/server';
import { getDatasets } from '@/lib/db/datasets';

/**
 * 获取导出数据集
 */
export async function GET(request, { params }) {
  try {
    const { projectId } = params;
    const { searchParams } = new URL(request.url);
    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: '项目ID不能为空' }, { status: 400 });
    }
    let status = searchParams.get('status');
    let confirmed = undefined;
    if (status === 'confirmed') confirmed = true;
    if (status === 'unconfirmed') confirmed = false;
    // 获取数据集
    let datasets = await getDatasets(projectId, confirmed);
    return NextResponse.json(datasets);
  } catch (error) {
    console.error('获取数据集失败:', String(error));
    return NextResponse.json(
      {
        error: error.message || '获取数据集失败'
      },
      { status: 500 }
    );
  }
}

'use client';

import { Paper, Grid, Box, Typography, useMediaQuery, Avatar } from '@mui/material';
import { styles } from '@/styles/home';
import { useTheme } from '@mui/material';
import { motion } from 'framer-motion';
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
import QuestionAnswerIcon from '@mui/icons-material/QuestionAnswer';
import StorageIcon from '@mui/icons-material/Storage';
import MemoryIcon from '@mui/icons-material/Memory';

// 默认模型列表
const mockModels = [
  { id: 'deepseek-r1', provider: 'Ollama', name: 'DeepSeek-R1' },
  { id: 'gpt-3.5-turbo-openai', provider: 'OpenAI', name: 'gpt-3.5-turbo' },
  { id: 'gpt-3.5-turbo-guiji', provider: 'Guiji', name: 'gpt-3.5-turbo' },
  { id: 'glm-4-flash', provider: 'Zhipu AI', name: 'GLM-4-Flash' }
];

export default function StatsCard({ projects }) {
  const theme = useTheme();
  const isMobile = useMediaQuery(theme.breakpoints.down('sm'));

  // 统计卡片数据
  const statsItems = [
    {
      value: projects.length,
      label: t('stats.ongoingProjects'),
      color: 'primary',
      icon: <FolderOpenIcon />
    },
    {
      value: projects.reduce((sum, project) => sum + (project.questionsCount || 0), 0),
      label: t('stats.questionCount'),
      color: 'secondary',
      icon: <QuestionAnswerIcon />
    },
    {
      value: projects.reduce((sum, project) => sum + (project.datasetsCount || 0), 0),
      label: t('stats.generatedDatasets'),
      color: 'success',
      icon: <StorageIcon />
    },
    {
      value: mockModels.length,
      label: t('stats.supportedModels'),
      color: 'warning',
      icon: <MemoryIcon />
    }
  ];

  return (
    <Paper
      elevation={0}
      sx={styles.statsCard(theme)}
      component={motion.div}
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5, delay: 0.2 }}
    >
      <Grid container spacing={3}>
        {statsItems.map((item, index) => (
          <Grid item xs={12} sm={6} md={3} key={index}>
            <Box
              sx={{
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                justifyContent: 'center',
                p: 2,
                borderRadius: 2,
                transition: 'all 0.3s ease',
                '&:hover': {
                  transform: 'translateY(-5px)',
                  boxShadow: `0 10px 20px ${theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.05)'}`
                }
              }}
              component={motion.div}
              whileHover={{ scale: 1.03 }}
              transition={{ type: 'spring', stiffness: 300 }}
            >
              <Avatar
                sx={{
                  width: 56,
                  height: 56,
                  mb: 2,
                  bgcolor: theme.palette[item.color].main,
                  color: '#fff',
                  boxShadow: `0 4px 12px ${theme.palette[item.color].main}40`
                }}
              >
                {item.icon}
              </Avatar>
              <Typography
                color={item.color + '.main'}
                variant={isMobile ? 'h3' : 'h2'}
                fontWeight="bold"
                sx={{
                  mb: 0.5,
                  background: `linear-gradient(135deg, ${theme.palette[item.color].main} 0%, ${theme.palette[item.color].light} 100%)`,
                  WebkitBackgroundClip: 'text',
                  WebkitTextFillColor: 'transparent',
                  backgroundClip: 'text',
                  textFillColor: 'transparent'
                }}
              >
                {item.value}
              </Typography>
              <Typography variant="subtitle1" color="text.secondary" fontWeight="500" sx={{ opacity: 0.8 }}>
                {item.label}
              </Typography>
            </Box>
          </Grid>
        ))}
      </Grid>
    </Paper>
  );
}

/**
 * 封装的通用重试函数，用于在操作失败后自动重试
 * @param {Function} asyncOperation - 需要执行的异步操作函数
 * @param {Object} options - 配置选项
 * @param {number} options.retries - 重试次数，默认为1
 * @param {number} options.delay - 重试前的延迟时间(毫秒)，默认为0
 * @param {Function} options.onRetry - 重试前的回调函数，接收错误和当前重试次数作为参数
 * @returns {Promise} - 返回异步操作的结果
 */
export const withRetry = async (asyncOperation, options = {}) => {
  const { retries = 1, delay = 0, onRetry = null } = options;
  let lastError;

  // 尝试执行操作，包括初次尝试和后续重试
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      return await asyncOperation();
    } catch (error) {
      lastError = error;

      // 如果这是最后一次尝试，则不再重试
      if (attempt === retries) {
        break;
      }

      // 如果提供了重试回调，则执行
      if (onRetry && typeof onRetry === 'function') {
        onRetry(error, attempt + 1);
      }

      // 如果设置了延迟，则等待指定时间
      if (delay > 0) {
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }

  // 如果所有尝试都失败，则抛出最后一个错误
  throw lastError;
};

/**
 * 封装的fetch函数，支持自动重试
 * @param {string} url - 请求URL
 * @param {Object} options - fetch选项
 * @param {Object} retryOptions - 重试选项
 * @returns {Promise} - 返回fetch响应
 */
export const fetchWithRetry = async (url, options = {}, retryOptions = {}) => {
  return withRetry(() => fetch(url, options), retryOptions);
};

/**
 * 封装的 fetch 函数
 * @param {string} url - 请求URL
 * @param {Object} options - fetch选项
 * @returns {Promise} - 返回fetch响应
 */
export const request = async (url, options = {}) => {
  try {
    const result = await fetch(url, options);
    const data = await result.json();
    if (!result.ok) {
      throw new Error(`Fetch Error: ${url} ${String(data.error || 'Unknown error')}`);
    }
    return data;
  } catch (error) {
    throw new Error(`Fetch Error: ${url}  ${String(error)} ${options.errMsg || ''}`);
  }
};

export default fetchWithRetry;

'use client';

import { useState } from 'react';
import {
  ListItem,
  ListItemIcon,
  ListItemText,
  Box,
  Typography,
  Chip,
  IconButton,
  Tooltip,
  CircularProgress
} from '@mui/material';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import DeleteIcon from '@mui/icons-material/Delete';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import { useTranslation } from 'react-i18next';

/**
 * 问题列表项组件
 * @param {Object} props
 * @param {Object} props.question - 问题对象
 * @param {number} props.level - 缩进级别
 * @param {Function} props.onDelete - 删除问题的回调
 * @param {Function} props.onGenerateDataset - 生成数据集的回调
 * @param {boolean} props.processing - 是否正在处理
 */
export default function QuestionListItem({ question, level, onDelete, onGenerateDataset, processing = false }) {
  const { t } = useTranslation();

  return (
    <ListItem
      sx={{
        pl: (level + 1) * 2,
        py: 0.75,
        borderLeft: '1px dashed rgba(0, 0, 0, 0.1)',
        ml: 2,
        borderBottom: '1px solid',
        borderColor: 'divider',
        '&:hover': {
          bgcolor: 'action.hover'
        }
      }}
      secondaryAction={
        <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
          <Tooltip title={t('datasets.generateDataset')}>
            <IconButton size="small" color="primary" onClick={e => onGenerateDataset(e)} disabled={processing}>
              {processing ? <CircularProgress size={16} /> : <AutoFixHighIcon fontSize="small" />}
            </IconButton>
          </Tooltip>
          <Tooltip title={t('common.delete')}>
            <IconButton size="small" color="error" onClick={e => onDelete(e)} disabled={processing}>
              <DeleteIcon fontSize="small" />
            </IconButton>
          </Tooltip>
        </Box>
      }
    >
      <ListItemIcon sx={{ minWidth: 32, color: 'secondary.main' }}>
        <HelpOutlineIcon fontSize="small" />
      </ListItemIcon>
      <ListItemText
        primary={
          <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
            <Typography
              variant="body2"
              sx={{
                whiteSpace: 'normal',
                wordBreak: 'break-word',
                paddingRight: '28px' // 留出删除按钮的空间
              }}
            >
              {question.question}
            </Typography>
            {question.answered && (
              <Chip
                size="small"
                label={t('datasets.answered')}
                color="success"
                variant="outlined"
                sx={{ height: 20, fontSize: '0.7rem' }}
              />
            )}
          </Box>
        }
      />
    </ListItem>
  );
}

'use client';

import { useState, useEffect } from 'react';
import { Box, Container, Typography, Paper, Divider, useTheme, alpha } from '@mui/material';
import StorageIcon from '@mui/icons-material/Storage';
import Navbar from '@/components/Navbar';
import { DatasetSearchBar } from '@/components/dataset-square/DatasetSearchBar';
import { DatasetSiteList } from '@/components/dataset-square/DatasetSiteList';
import { useTranslation } from 'react-i18next';

export default function DatasetSquarePage() {
  const [projects, setProjects] = useState([]);
  const theme = useTheme();
  const { t } = useTranslation();

  // 获取项目列表和模型列表
  useEffect(() => {
    async function fetchData() {
      try {
        // 获取用户创建的项目详情
        const response = await fetch('/api/projects');
        if (response.ok) {
          const projectsData = await response.json();
          setProjects(projectsData);
        }
      } catch (error) {
        console.error('获取数据失败:', error);
      }
    }

    fetchData();
  }, []);

  return (
    <main>
      {/* 导航栏 */}
      <Navbar projects={projects} />

      {/* 头部区域 */}
      <Box
        sx={{
          pt: 10,
          pb: 8,
          background:
            theme.palette.mode === 'dark'
              ? `linear-gradient(135deg, ${alpha(theme.palette.primary.dark, 0.6)} 0%, ${alpha(theme.palette.background.default, 0.9)} 100%)`
              : `linear-gradient(135deg, ${alpha(theme.palette.primary.main, 0.9)} 0%, ${alpha(theme.palette.primary.light, 0.7)} 100%)`,
          borderBottom: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
          position: 'relative',
          boxShadow: '0 4px 20px rgba(0,0,0,0.1)'
        }}
      >
        {/* 背景装饰 */}
        <Box
          sx={{
            position: 'absolute',
            top: -100,
            left: -100,
            width: 300,
            height: 300,
            borderRadius: '50%',
            background: `radial-gradient(circle, ${alpha(theme.palette.secondary.main, 0.2)} 0%, transparent 70%)`,
            filter: 'blur(40px)',
            zIndex: 0
          }}
        />
        <Box
          sx={{
            position: 'absolute',
            bottom: -80,
            right: -80,
            width: 350,
            height: 350,
            borderRadius: '50%',
            background: `radial-gradient(circle, ${alpha(theme.palette.primary.light, 0.15)} 0%, transparent 70%)`,
            filter: 'blur(30px)',
            zIndex: 0
          }}
        />
        <Box
          sx={{
            position: 'absolute',
            top: '40%',
            right: '10%',
            width: 100,
            height: 100,
            borderRadius: '50%',
            background: `radial-gradient(circle, ${alpha(theme.palette.secondary.light, 0.15)} 0%, transparent 70%)`,
            filter: 'blur(20px)',
            zIndex: 0
          }}
        />

        <Container maxWidth="lg" sx={{ position: 'relative', zIndex: 1 }}>
          <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', mb: 3 }}>
            <StorageIcon
              sx={{
                fontSize: 48,
                mr: 2.5,
                color: theme.palette.mode === 'dark' ? theme.palette.secondary.light : 'white',
                filter: 'drop-shadow(0 2px 5px rgba(0,0,0,0.2))'
              }}
            />
            <Typography
              variant="h2"
              component="h1"
              sx={{
                fontWeight: 800,
                color: 'white',
                textShadow: '0 2px 8px rgba(0,0,0,0.15)',
                mb: 0,
                letterSpacing: '-0.5px',
                background:
                  theme.palette.mode === 'dark'
                    ? 'linear-gradient(90deg, #ffffff 0%, #e0e0e0 100%)'
                    : 'linear-gradient(90deg, #ffffff 30%, #f5f5f5 100%)',
                WebkitBackgroundClip: 'text',
                WebkitTextFillColor: 'transparent',
                backgroundClip: 'text',
                textFillColor: 'transparent'
              }}
            >
              {t('datasetSquare.title')}
            </Typography>
          </Box>
          <Typography
            variant="h5"
            align="center"
            sx={{
              maxWidth: 800,
              mx: 'auto',
              mb: 6,
              color: theme.palette.mode === 'dark' ? alpha('#FFFFFF', 0.9) : '#FFFFFF',
              fontWeight: 400,
              lineHeight: 1.6,
              textShadow: '0 1px 3px rgba(0,0,0,0.1)'
            }}
          >
            {t('datasetSquare.subtitle')}
          </Typography>

          {/* 搜索栏组件 */}
          <Box
            sx={{
              maxWidth: 800,
              mx: 'auto',
              position: 'relative',
              zIndex: 10
            }}
          >
            <Paper
              elevation={6}
              sx={{
                width: '100%',
                p: 2.5,
                borderRadius: 3,
                background: 'transparent',
                backdropFilter: 'blur(10px)',
                boxShadow:
                  theme.palette.mode === 'dark' ? '0 10px 40px rgba(0,0,0,0.3)' : '0 10px 40px rgba(0,0,0,0.15)',
                border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
                transform: 'translateY(0)',
                transition: 'transform 0.3s ease, box-shadow 0.3s ease',
                '&:hover': {
                  transform: 'translateY(-5px)',
                  boxShadow:
                    theme.palette.mode === 'dark' ? '0 15px 50px rgba(0,0,0,0.4)' : '0 15px 50px rgba(0,0,0,0.2)'
                },
                overflow: 'visible' // 确保不裁剪子元素
              }}
            >
              <DatasetSearchBar />
            </Paper>
          </Box>
        </Container>
      </Box>

      {/* 内容区域 */}
      <Container maxWidth="lg" sx={{ py: 8 }}>
        {/* 数据集网站列表组件 */}
        <Paper
          elevation={0}
          sx={{
            p: { xs: 2, sm: 4 },
            borderRadius: 3,
            bgcolor:
              theme.palette.mode === 'dark'
                ? alpha(theme.palette.background.paper, 0.6)
                : theme.palette.background.paper,
            backdropFilter: 'blur(8px)',
            border: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
            boxShadow: theme.palette.mode === 'dark' ? '0 4px 20px rgba(0,0,0,0.2)' : '0 4px 20px rgba(0,0,0,0.05)'
          }}
        >
          <DatasetSiteList />
        </Paper>
      </Container>
    </main>
  );
}

import { NextResponse } from 'next/server';
import { deleteQuestion } from '@/lib/db/questions';

// 删除单个问题
export async function DELETE(request, { params }) {
  try {
    const { projectId, questionId } = params;

    // 验证参数
    if (!projectId) {
      return NextResponse.json({ error: 'Project ID is required' }, { status: 400 });
    }

    if (!questionId) {
      return NextResponse.json({ error: 'Question ID is required' }, { status: 400 });
    }

    // 删除问题
    await deleteQuestion(questionId);

    return NextResponse.json({ success: true, message: 'Delete successful' });
  } catch (error) {
    console.error('Delete failed:', String(error));
    return NextResponse.json({ error: error.message || 'Delete failed' }, { status: 500 });
  }
}

import { NextResponse } from 'next/server';
import path from 'path';
import fs from 'fs';
import { getProjectRoot } from '@/lib/db/base';

export async function GET(request, { params }) {
  try {
    const { projectId } = params;
    if (!projectId) {
      return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
    }

    const projectRoot = await getProjectRoot();
    const projectPath = path.join(projectRoot, projectId);
    const configPath = path.join(projectPath, 'dataset_info.json');

    const exists = fs.existsSync(configPath);

    return NextResponse.json({
      exists,
      configPath: exists ? configPath : null
    });
  } catch (error) {
    console.error('Error checking Llama Factory config:', String(error));
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

import { NextResponse } from 'next/server';
import { getChunkByName } from '@/lib/db/chunks';

/**
 * 根据文本块名称获取文本块
 * @param {Request} request 请求对象
 * @param {object} context 上下文，包含路径参数
 * @returns {Promise<NextResponse>} 响应对象
 */
export async function GET(request, { params }) {
  try {
    const { projectId } = params;

    // 从查询参数中获取 chunkName
    const { searchParams } = new URL(request.url);
    const chunkName = searchParams.get('chunkName');

    if (!chunkName) {
      return NextResponse.json({ error: '文本块名称不能为空' }, { status: 400 });
    }

    // 根据名称和项目ID查询文本块
    const chunk = await getChunkByName(projectId, chunkName);

    if (!chunk) {
      return NextResponse.json({ error: '未找到指定的文本块' }, { status: 404 });
    }

    // 返回文本块信息
    return NextResponse.json(chunk);
  } catch (error) {
    console.error('根据名称获取文本块失败:', String(error));
    return NextResponse.json({ error: '获取文本块失败: ' + error.message }, { status: 500 });
  }
}

'use client';

import { useState, useEffect } from 'react';
import { Grid, Box, Typography, Skeleton, Divider, Tabs, Tab, Fade, Chip, useTheme, alpha, Paper } from '@mui/material';
import StorageIcon from '@mui/icons-material/Storage';
import CategoryIcon from '@mui/icons-material/Category';
import StarIcon from '@mui/icons-material/Star';
import { DatasetSiteCard } from './DatasetSiteCard';
import sites from '@/constant/sites.json';
import { useTranslation } from 'react-i18next';

export function DatasetSiteList() {
  const [loading, setLoading] = useState(true);
  const theme = useTheme();
  const { t } = useTranslation();

  // 定义类别
  const CATEGORIES = {
    ALL: t('datasetSquare.categories.all'),
    POPULAR: t('datasetSquare.categories.popular'),
    CHINESE: t('datasetSquare.categories.chinese'),
    ENGLISH: t('datasetSquare.categories.english'),
    RESEARCH: t('datasetSquare.categories.research'),
    MULTIMODAL: t('datasetSquare.categories.multimodal')
  };
  const [activeCategory, setActiveCategory] = useState(CATEGORIES.ALL);

  // 模拟加载效果
  useEffect(() => {
    const timer = setTimeout(() => {
      setLoading(false);
    }, 800);

    return () => clearTimeout(timer);
  }, []);

  // 处理类别切换
  const handleCategoryChange = (event, newValue) => {
    setActiveCategory(newValue);
  };

  // 根据当前选中的类别过滤网站
  const getFilteredSites = () => {
    if (activeCategory === CATEGORIES.ALL) {
      return sites;
    } else if (activeCategory === CATEGORIES.POPULAR) {
      return sites.filter(site => site.labels && site.labels.includes(t('datasetSquare.categories.popular')));
    } else if (activeCategory === CATEGORIES.CHINESE) {
      return sites.filter(site => site.labels && site.labels.includes(t('datasetSquare.categories.chinese')));
    } else if (activeCategory === CATEGORIES.ENGLISH) {
      return sites.filter(site => site.labels && site.labels.includes(t('datasetSquare.categories.english')));
    } else if (activeCategory === CATEGORIES.RESEARCH) {
      return sites.filter(site => site.labels && site.labels.includes(t('datasetSquare.categories.research')));
    } else if (activeCategory === CATEGORIES.MULTIMODAL) {
      return sites.filter(site => site.labels && site.labels.includes(t('datasetSquare.categories.multimodal')));
    }
    return sites;
  };

  const filteredSites = getFilteredSites();

  return (
    <Box sx={{ mt: 4 }}>
      {/* 类别选择器 */}
      <Box sx={{ mb: 4 }}>
        <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
          <CategoryIcon sx={{ mr: 1, color: 'primary.main' }} />
          <Typography variant="h5" component="h2" fontWeight={600}>
            {t('datasetSquare.categoryTitle')}
          </Typography>
        </Box>

        <Paper
          elevation={0}
          sx={{
            borderRadius: 2,
            p: 0.5,
            bgcolor:
              theme.palette.mode === 'dark'
                ? alpha(theme.palette.primary.dark, 0.1)
                : alpha(theme.palette.primary.light, 0.1),
            border: `1px solid ${alpha(theme.palette.primary.main, 0.1)}`,
            overflow: 'auto'
          }}
        >
          <Tabs
            value={activeCategory}
            onChange={handleCategoryChange}
            variant="scrollable"
            scrollButtons="auto"
            sx={{
              minHeight: 48,
              '& .MuiTabs-indicator': {
                display: 'none'
              },
              '& .MuiTab-root': {
                minHeight: 40,
                borderRadius: 2,
                mx: 0.5,
                transition: 'all 0.2s',
                '&.Mui-selected': {
                  bgcolor:
                    theme.palette.mode === 'dark' ? alpha(theme.palette.primary.main, 0.2) : theme.palette.primary.main,
                  color: theme.palette.mode === 'dark' ? theme.palette.primary.light : 'white',
                  fontWeight: 600
                }
              }
            }}
          >
            <Tab
              value={CATEGORIES.ALL}
              label={CATEGORIES.ALL}
              icon={<StorageIcon fontSize="small" />}
              iconPosition="start"
            />
            <Tab
              value={CATEGORIES.POPULAR}
              label={CATEGORIES.POPULAR}
              icon={<StarIcon fontSize="small" />}
              iconPosition="start"
            />
            <Tab value={CATEGORIES.CHINESE} label={CATEGORIES.CHINESE} />
            <Tab value={CATEGORIES.ENGLISH} label={CATEGORIES.ENGLISH} />
            <Tab value={CATEGORIES.RESEARCH} label={CATEGORIES.RESEARCH} />
            <Tab value={CATEGORIES.MULTIMODAL} label={CATEGORIES.MULTIMODAL} />
          </Tabs>
        </Paper>
      </Box>

      {/* 数据集网站列表 */}
      <Box sx={{ position: 'relative', minHeight: 300 }}>
        {loading ? (
          // 加载骨架屏
          <Grid container spacing={3}>
            {Array.from(new Array(8)).map((_, index) => (
              <Grid item xs={12} sm={6} md={4} lg={3} key={index}>
                <Box sx={{ width: '100%', height: '100%' }}>
                  <Skeleton variant="rectangular" height={160} sx={{ borderRadius: 2 }} />
                  <Box sx={{ pt: 1.5, px: 0.5 }}>
                    <Skeleton width="80%" height={28} />
                    <Skeleton width="100%" />
                    <Skeleton width="100%" />
                    <Skeleton width="60%" />
                  </Box>
                </Box>
              </Grid>
            ))}
          </Grid>
        ) : (
          <Fade in={!loading} timeout={500}>
            <Box>
              {/* 结果数量提示 */}
              <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
                <Typography variant="subtitle1" color="text.secondary">
                  {t('datasetSquare.foundResources', { count: filteredSites.length })}{' '}
                  <Chip
                    label={filteredSites.length}
                    size="small"
                    color="primary"
                    sx={{ mx: 0.5, height: 20, fontSize: '0.75rem' }}
                  />
                </Typography>

                {activeCategory !== CATEGORIES.ALL && (
                  <Chip
                    label={t('datasetSquare.currentFilter', { category: activeCategory })}
                    size="small"
                    onDelete={() => setActiveCategory(CATEGORIES.ALL)}
                    sx={{ borderRadius: 1.5 }}
                  />
                )}
              </Box>

              {filteredSites.length > 0 ? (
                <Grid container spacing={3}>
                  {filteredSites.map((site, index) => (
                    <Grid item xs={12} sm={6} md={4} lg={3} key={index}>
                      <DatasetSiteCard site={site} />
                    </Grid>
                  ))}
                </Grid>
              ) : (
                <Box
                  sx={{
                    py: 10,
                    display: 'flex',
                    flexDirection: 'column',
                    alignItems: 'center',
                    bgcolor:
                      theme.palette.mode === 'dark'
                        ? alpha(theme.palette.background.paper, 0.2)
                        : alpha(theme.palette.grey[100], 0.5),
                    borderRadius: 2
                  }}
                >
                  <StorageIcon sx={{ fontSize: 60, color: 'text.disabled', mb: 2 }} />
                  <Typography variant="h6" color="text.secondary" gutterBottom>
                    {t('datasetSquare.noDatasets')}
                  </Typography>
                  <Typography variant="body2" color="text.disabled">
                    {t('datasetSquare.tryOtherCategories')}
                  </Typography>
                </Box>
              )}
            </Box>
          </Fade>
        )}
      </Box>
    </Box>
  );
}

const { autoUpdater } = require('electron-updater');
const { getAppVersion } = require('../util');

/**
 * 设置自动更新
 * @param {BrowserWindow} mainWindow 主窗口
 */
function setupAutoUpdater(mainWindow) {
  autoUpdater.autoDownload = false;
  autoUpdater.allowDowngrade = false;

  // 检查更新时出错
  autoUpdater.on('error', error => {
    if (mainWindow) {
      mainWindow.webContents.send('update-error', error.message);
    }
  });

  // 检查到更新时
  autoUpdater.on('update-available', info => {
    if (mainWindow) {
      mainWindow.webContents.send('update-available', {
        version: info.version,
        releaseDate: info.releaseDate,
        releaseNotes: info.releaseNotes
      });
    }
  });

  // 没有可用更新
  autoUpdater.on('update-not-available', () => {
    if (mainWindow) {
      mainWindow.webContents.send('update-not-available');
    }
  });

  // 下载进度
  autoUpdater.on('download-progress', progressObj => {
    if (mainWindow) {
      mainWindow.webContents.send('download-progress', progressObj);
    }
  });

  // 下载完成
  autoUpdater.on('update-downloaded', info => {
    if (mainWindow) {
      mainWindow.webContents.send('update-downloaded', {
        version: info.version,
        releaseDate: info.releaseDate,
        releaseNotes: info.releaseNotes
      });
    }
  });
}

/**
 * 检查更新
 * @param {boolean} isDev 是否为开发环境
 * @returns {Promise<Object>} 更新信息
 */
async function checkUpdate(isDev) {
  try {
    if (isDev) {
      // 开发环境下模拟更新检查
      return {
        hasUpdate: false,
        currentVersion: getAppVersion(),
        message: '开发环境下不检查更新'
      };
    }

    // 返回当前版本信息，并开始检查更新
    const result = await autoUpdater.checkForUpdates();
    return {
      checking: true,
      currentVersion: getAppVersion()
    };
  } catch (error) {
    console.error('检查更新失败:', error);
    return {
      hasUpdate: false,
      currentVersion: getAppVersion(),
      error: error.message
    };
  }
}

/**
 * 下载更新
 * @returns {Promise<Object>} 下载状态
 */
async function downloadUpdate() {
  try {
    autoUpdater.downloadUpdate();
    return { downloading: true };
  } catch (error) {
    console.error('下载更新失败:', error);
    return { error: error.message };
  }
}

/**
 * 安装更新
 * @returns {Object} 安装状态
 */
function installUpdate() {
  autoUpdater.quitAndInstall(false, true);
  return { installing: true };
}

module.exports = {
  setupAutoUpdater,
  checkUpdate,
  downloadUpdate,
  installUpdate
};

// 并行处理数组的辅助函数，限制并发数
export const processInParallel = async (items, processFunction, concurrencyLimit, onProgress) => {
  const results = [];
  const inProgress = new Set();
  const queue = [...items];
  let completedCount = 0;

  while (queue.length > 0 || inProgress.size > 0) {
    // 如果有空闲槽位且队列中还有任务，启动新任务
    while (inProgress.size < concurrencyLimit && queue.length > 0) {
      const item = queue.shift();
      const promise = processFunction(item).then(result => {
        inProgress.delete(promise);
        onProgress && onProgress(++completedCount, items.length);
        return result;
      });
      inProgress.add(promise);
      results.push(promise);
    }

    // 等待其中一个任务完成
    if (inProgress.size > 0) {
      await Promise.race(inProgress);
    }
  }

  return Promise.all(results);
};

import { NextResponse } from 'next/server';
import { db } from '@/lib/db';

/**
 * 获取项目的所有蒸馏标签
 */
export async function GET(request, { params }) {
  try {
    const { projectId } = params;

    // 验证项目ID
    if (!projectId) {
      return NextResponse.json({ error: '项目ID不能为空' }, { status: 400 });
    }

    // 获取所有标签
    const tags = await db.tags.findMany({
      where: {
        projectId
      },
      orderBy: {
        label: 'asc'
      }
    });

    return NextResponse.json(tags);
  } catch (error) {
    console.error('获取蒸馏标签失败:', String(error));
    return NextResponse.json({ error: error.message || '获取蒸馏标签失败' }, { status: 500 });
  }
}

'use client';

import { Box, Button, Divider, Typography, IconButton, CircularProgress, Paper, Tooltip } from '@mui/material';
import NavigateBeforeIcon from '@mui/icons-material/NavigateBefore';
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import DeleteIcon from '@mui/icons-material/Delete';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/navigation';

/**
 * 数据集详情页面的头部导航组件
 */
export default function DatasetHeader({
  projectId,
  datasetsAllCount,
  datasetsConfirmCount,
  confirming,
  currentDataset,
  shortcutsEnabled,
  setShortcutsEnabled,
  onNavigate,
  onConfirm,
  onDelete
}) {
  const router = useRouter();
  const { t } = useTranslation();

  return (
    <Paper sx={{ p: 2, mb: 3 }}>
      <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
          <Button startIcon={<NavigateBeforeIcon />} onClick={() => router.push(`/projects/${projectId}/datasets`)}>
            {t('common.backToList')}
          </Button>
          <Divider orientation="vertical" flexItem />
          <Typography variant="h6">{t('datasets.datasetDetail')}</Typography>
          <Typography variant="body2" color="text.secondary">
            {t('datasets.stats', {
              total: datasetsAllCount,
              confirmed: datasetsConfirmCount,
              percentage: ((datasetsConfirmCount / datasetsAllCount) * 100).toFixed(2)
            })}
          </Typography>
        </Box>
        {/* 快捷键启用选项 - 已注释掉，保持原代码结构 */}
        {/* <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
          <Typography variant="body1">{t('datasets.enableShortcuts')}</Typography>
          <Tooltip title={t('datasets.shortcutsHelp')}>
            <IconButton size="small" color="info">
              <Typography variant="body1" sx={{ fontWeight: 'bold' }}>?</Typography>
            </IconButton>
          </Tooltip>
          <Button
            variant={shortcutsEnabled ? 'contained' : 'outlined'}
            onClick={() => setShortcutsEnabled((prev) => !prev)}
          >
            {shortcutsEnabled ? t('common.enabled') : t('common.disabled')}
          </Button>
        </Box> */}
        <Box sx={{ display: 'flex', gap: 1 }}>
          <IconButton onClick={() => onNavigate('prev')}>
            <NavigateBeforeIcon />
          </IconButton>
          <IconButton onClick={() => onNavigate('next')}>
            <NavigateNextIcon />
          </IconButton>
          <Divider orientation="vertical" flexItem />
          <Button
            variant="contained"
            color="primary"
            disabled={confirming || currentDataset.confirmed}
            onClick={onConfirm}
            sx={{ mr: 1 }}
          >
            {confirming ? (
              <CircularProgress size={24} />
            ) : currentDataset.confirmed ? (
              t('datasets.confirmed')
            ) : (
              t('datasets.confirmSave')
            )}
          </Button>
          <Button variant="outlined" color="error" startIcon={<DeleteIcon />} onClick={onDelete}>
            {t('common.delete')}
          </Button>
        </Box>
      </Box>
    </Paper>
  );
}

/**
 * Enhanced answer generation prompt template - based on GA pairs
 * @param {Object} params - Parameter object
 * @param {string} params.text - Reference text content
 * @param {string} params.question - Question content
 * @param {string} params.language - Language
 * @param {string} params.globalPrompt - Global prompt
 * @param {string} params.answerPrompt - Answer prompt
 * @param {Array} params.gaPairs - GA pairs array containing genre and audience information
 * @param {Object} params.activeGaPair - Currently active GA pair
 */
export default function getEnhancedAnswerEnPrompt({
  text,
  question,
  globalPrompt = '',
  answerPrompt = '',
  activeGaPair = null
}) {
  if (globalPrompt) {
    globalPrompt = `In subsequent tasks, you must strictly follow these rules: ${globalPrompt}`;
  }
  if (answerPrompt) {
    answerPrompt = `In generating answers, you must strictly follow these rules: ${answerPrompt}`;
  }

  // Build GA pairs related prompts
  let gaPrompt = '';
  if (activeGaPair && activeGaPair.active) {
    gaPrompt = `
## Special Requirements - Genre & Audience Adaptation (MGA):
Adjust your response style and depth according to the following genre and audience combination:

**Current Genre**: ${activeGaPair.genre}
**Target Audience**: ${activeGaPair.audience}

Please ensure:
1. The organization, style, level of detail, and language of the answer should fully comply with the requirements of "${activeGaPair.genre}".
2. The answer should consider the comprehension ability and knowledge background of "${activeGaPair.audience}", striving for clarity and ease of understanding.
3. Word choice and explanation detail match the target audience's knowledge background.
4. Maintain content accuracy and professionalism while enhancing specificity.
5. If "${activeGaPair.genre}" or "${activeGaPair.audience}" suggests the need, the answer can appropriately include explanations, examples, or steps.
6. The answer should directly address the question, ensuring the logic and coherence of the Q&A. It should not include irrelevant information or citation marks, such as content mentioned in GA pairs, to prevent contaminating the data generation results.
`;
  } else {
    gaPrompt = '';
  }

  return `
# Role: Fine-tuning Dataset Generation Expert (MGA Enhanced)
## Profile:
- Description: You are an expert in generating fine-tuning datasets, skilled at generating accurate answers to questions from the given content, and capable of adjusting response style according to Genre-Audience combinations to ensure accuracy, relevance, and specificity of answers.
${globalPrompt}

## Skills:
1. The answer must be based on the given content.
2. The answer must be accurate and not fabricated.
3. The answer must be relevant to the question.
4. The answer must be logical.
5. Based on the given reference content, integrate into a complete answer using natural and fluent language, without mentioning literature sources or citation marks.
6. Ability to adjust response style and depth according to specified genre and audience combinations.
7. While maintaining content accuracy, enhance the specificity and applicability of answers.

${gaPrompt}

## Workflow:
1. Take a deep breath and work on this problem step-by-step.
2. First, analyze the given file content and question type.
3. Then, extract key information from the content.
4. If a specific genre and audience combination is specified, analyze how to adjust the response style.
5. Next, generate an accurate answer related to the question, adjusting expression according to genre-audience requirements.
6. Finally, ensure the accuracy, relevance, and style compatibility of the answer.

## Reference Content:
${text}

## Question
${question}

## Constraints:
1. The answer must be based on the given content.
2. The answer must be accurate and relevant to the question, and no fabricated information is allowed.
3. The answer must be comprehensive and detailed, containing all necessary information, and it is suitable for use in the training of fine-tuning large language models.
4. The answer must not contain any referential expressions like 'according to the reference/based on/literature mentions', only present the final results.
5. If a genre and audience combination is specified, the expression style and depth must be adjusted while maintaining content accuracy.
6. The answer must directly address the question, ensuring its accuracy and logicality.
${answerPrompt}
`;
}

'use client';

import { Card, CardActionArea, CardContent, CardMedia, Typography, Box, Chip, useTheme, alpha } from '@mui/material';
import LaunchIcon from '@mui/icons-material/Launch';
import StorageIcon from '@mui/icons-material/Storage';
import { useTranslation } from 'react-i18next';

export function DatasetSiteCard({ site }) {
  const { name, link, description, image, labels } = site;
  const theme = useTheme();

  // 处理图片路径，如果没有图片则使用默认图片
  const imageUrl = image || `/imgs/default-dataset.png`;
  const { t } = useTranslation();

  // 处理卡片点击
  const handleCardClick = () => {
    window.open(link, '_blank');
  };

  return (
    <Card
      sx={{
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        transition: 'all 0.3s ease',
        borderRadius: 2,
        overflow: 'hidden',
        boxShadow: theme.palette.mode === 'dark' ? '0 4px 20px rgba(0,0,0,0.3)' : '0 4px 20px rgba(0,0,0,0.1)',
        '&:hover': {
          transform: 'translateY(-6px)',
          boxShadow: theme.palette.mode === 'dark' ? '0 8px 30px rgba(0,0,0,0.4)' : '0 8px 30px rgba(0,0,0,0.15)',
          '& .MuiCardMedia-root': {
            transform: 'scale(1.05)'
          }
        }
      }}
    >
      <CardActionArea
        onClick={handleCardClick}
        sx={{
          flexGrow: 1,
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'stretch',
          height: '100%',
          '&:hover': {
            '& .card-content': {
              background:
                theme.palette.mode === 'dark'
                  ? alpha(theme.palette.primary.dark, 0.1)
                  : alpha(theme.palette.primary.light, 0.1)
            }
          }
        }}
      >
        {/* 网站截图 */}
        <Box sx={{ position: 'relative', width: '100%', height: 160, overflow: 'hidden' }}>
          <CardMedia
            component="img"
            height="160"
            image={imageUrl}
            alt={name}
            sx={{
              objectFit: 'cover',
              bgcolor: theme.palette.mode === 'dark' ? 'grey.900' : 'grey.100',
              transition: 'transform 0.5s ease'
            }}
          />
          <Box
            sx={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: '100%',
              background: `linear-gradient(to bottom, transparent 70%, ${theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.8)' : 'rgba(0,0,0,0.5)'})`,
              zIndex: 1
            }}
          />
          <Chip
            icon={<StorageIcon fontSize="small" />}
            label={t('datasetSquare.dataset')}
            size="small"
            sx={{
              position: 'absolute',
              top: 10,
              right: 10,
              zIndex: 2,
              backgroundColor: alpha(theme.palette.background.paper, 0.8),
              backdropFilter: 'blur(4px)',
              border: `1px solid ${alpha(theme.palette.primary.main, 0.2)}`,
              '& .MuiChip-icon': {
                color: theme.palette.primary.main
              }
            }}
          />
        </Box>

        {/* 网站信息 */}
        <CardContent
          className="card-content"
          sx={{
            flexGrow: 1,
            display: 'flex',
            flexDirection: 'column',
            transition: 'background 0.3s ease',
            p: 2.5
          }}
        >
          <Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 1.5 }}>
            <Typography
              variant="h6"
              component="div"
              sx={{
                fontWeight: 600,
                lineHeight: 1.3,
                mb: 0.5,
                pr: 2, // 留出空间给图标
                color: theme.palette.mode === 'dark' ? theme.palette.primary.light : theme.palette.primary.dark
              }}
            >
              {name}
            </Typography>
            <LaunchIcon
              fontSize="small"
              sx={{
                color: theme.palette.mode === 'dark' ? theme.palette.primary.light : theme.palette.primary.main,
                opacity: 0.8,
                mt: 0.5
              }}
            />
          </Box>

          <Typography
            variant="body2"
            sx={{
              overflow: 'hidden',
              textOverflow: 'ellipsis',
              display: '-webkit-box',
              WebkitLineClamp: 3,
              WebkitBoxOrient: 'vertical',
              color: theme.palette.text.secondary,
              lineHeight: 1.6,
              mb: 1
            }}
          >
            {description}
          </Typography>

          <Box sx={{ mt: 'auto', pt: 1.5 }}>
            {/* 标签显示 */}
            {labels && labels.length > 0 && (
              <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mb: 1.5 }}>
                {labels.map((label, index) => (
                  <Chip
                    key={index}
                    label={label}
                    size="small"
                    sx={{
                      borderRadius: 1,
                      height: 20,
                      fontSize: '0.65rem',
                      backgroundColor: alpha(theme.palette.primary.main, 0.1),
                      color: theme.palette.primary.main,
                      '&:hover': {
                        backgroundColor: alpha(theme.palette.primary.main, 0.2)
                      }
                    }}
                  />
                ))}
              </Box>
            )}

            <Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
              <Chip
                label={t('datasetSquare.viewDataset')}
                size="small"
                color="primary"
                variant="outlined"
                sx={{
                  borderRadius: 1,
                  height: 24,
                  fontSize: '0.75rem',
                  '&:hover': {
                    backgroundColor: alpha(theme.palette.primary.main, 0.1)
                  }
                }}
              />
            </Box>
          </Box>
        </CardContent>
      </CardActionArea>
    </Card>
  );
}

import { NextResponse } from 'next/server';
import { deleteModelConfigById } from '@/lib/db/model-config';

// 删除模型配置
export async function DELETE(request, { params }) {
  try {
    const { projectId, modelConfigId } = params;
    // 验证项目 ID
    if (!projectId) {
      return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
    }
    await deleteModelConfigById(modelConfigId);
    return NextResponse.json(true);
  } catch (error) {
    console.error('Error obtaining model configuration:', String(error));
    return NextResponse.json({ error: 'Failed to obtain model configuration' }, { status: 500 });
  }
}

'use client';

import { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import {
  Dialog,
  DialogTitle,
  DialogContent,
  Box,
  Typography,
  LinearProgress,
  Paper,
  Divider,
  IconButton,
  Button
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';

/**
 * 全自动蒸馏进度组件
 * @param {Object} props
 * @param {boolean} props.open - 对话框是否打开
 * @param {Function} props.onClose - 关闭对话框的回调
 * @param {Object} props.progress - 进度信息
 */
export default function AutoDistillProgress({ open, onClose, progress = {} }) {
  const { t } = useTranslation();
  const logContainerRef = useRef(null);

  // 自动滚动到底部
  useEffect(() => {
    if (logContainerRef.current) {
      logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
    }
  }, [progress.logs]);

  const getStageText = () => {
    const { stage } = progress;
    switch (stage) {
      case 'level1':
        return t('distill.stageBuildingLevel1');
      case 'level2':
        return t('distill.stageBuildingLevel2');
      case 'level3':
        return t('distill.stageBuildingLevel3');
      case 'level4':
        return t('distill.stageBuildingLevel4');
      case 'level5':
        return t('distill.stageBuildingLevel5');
      case 'questions':
        return t('distill.stageBuildingQuestions');
      case 'datasets':
        return t('distill.stageBuildingDatasets');
      case 'completed':
        return t('distill.stageCompleted');
      default:
        return t('distill.stageInitializing');
    }
  };

  const getOverallProgress = () => {
    const { tagsBuilt, tagsTotal, questionsBuilt, questionsTotal, datasetsBuilt, datasetsTotal } = progress;

    // 整体进度按比例计算：标签构建占30%，问题生成占35%，数据集生成占35%
    let tagProgress = tagsTotal ? (tagsBuilt / tagsTotal) * 30 : 0;
    let questionProgress = questionsTotal ? (questionsBuilt / questionsTotal) * 35 : 0;
    let datasetProgress = datasetsTotal ? (datasetsBuilt / datasetsTotal) * 35 : 0;

    return Math.min(100, Math.round(tagProgress + questionProgress + datasetProgress));
  };

  return (
    <Dialog
      open={open}
      onClose={progress.stage === 'completed' || !progress.stage ? onClose : null}
      maxWidth="md"
      fullWidth
    >
      <DialogTitle>
        <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          {t('distill.autoDistillProgress')}
          {(progress.stage === 'completed' || !progress.stage) && (
            <IconButton onClick={onClose} aria-label="close">
              <CloseIcon />
            </IconButton>
          )}
        </Box>
      </DialogTitle>
      <DialogContent>
        <Box sx={{ py: 1 }}>
          {/* 整体进度 */}
          <Box sx={{ mb: 4 }}>
            <Typography variant="h6" gutterBottom>
              {t('distill.overallProgress')}
            </Typography>

            <Box sx={{ mb: 2 }}>
              <LinearProgress variant="determinate" value={getOverallProgress()} sx={{ height: 10, borderRadius: 5 }} />
              <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 0.5 }}>
                <Typography variant="body2" color="text.secondary">
                  {getOverallProgress()}%
                </Typography>
              </Box>
            </Box>

            <Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 2 }}>
              <Paper variant="outlined" sx={{ p: 2 }}>
                <Typography variant="body2" color="text.secondary">
                  {t('distill.tagsProgress')}
                </Typography>
                <Typography variant="h6">
                  {progress.tagsBuilt || 0} / {progress.tagsTotal || 0}
                </Typography>
              </Paper>

              <Paper variant="outlined" sx={{ p: 2 }}>
                <Typography variant="body2" color="text.secondary">
                  {t('distill.questionsProgress')}
                </Typography>
                <Typography variant="h6">
                  {progress.questionsBuilt || 0} / {progress.questionsTotal || 0}
                </Typography>
              </Paper>

              <Paper variant="outlined" sx={{ p: 2 }}>
                <Typography variant="body2" color="text.secondary">
                  {t('distill.datasetsProgress')}
                </Typography>
                <Typography variant="h6">
                  {progress.datasetsBuilt || 0} / {progress.datasetsTotal || 0}
                </Typography>
              </Paper>
            </Box>
          </Box>

          {/* 当前阶段 */}
          <Box sx={{ mb: 4 }}>
            <Typography variant="h6" gutterBottom>
              {t('distill.currentStage')}
            </Typography>

            <Paper variant="outlined" sx={{ p: 2, bgcolor: 'primary.light', color: 'primary.contrastText' }}>
              <Typography variant="h6">{getStageText()}</Typography>
            </Paper>
          </Box>

          {/* 实时日志 */}
          <Box sx={{ mb: 2 }}>
            <Typography variant="h6" gutterBottom>
              {t('distill.realTimeLogs')}
            </Typography>

            <Paper
              variant="outlined"
              sx={{
                p: 2,
                maxHeight: 250,
                overflow: 'auto',
                bgcolor: 'grey.900',
                color: 'grey.100',
                fontFamily: 'monospace',
                fontSize: '0.875rem'
              }}
              ref={logContainerRef}
            >
              {progress.logs?.length > 0 ? (
                progress.logs.map((log, index) => {
                  // 检测成功日志，显示为绿色  Successfully
                  let color = 'inherit';
                  if (log.includes('成功') || log.includes('完成') || log.includes('Successfully')) {
                    color = '#4caf50';
                  }
                  if (log.includes('失败') || log.toLowerCase().includes('error')) {
                    color = '#f44336';
                  }
                  return (
                    <Box key={index} sx={{ mb: 0.5, color: color }}>
                      {log}
                    </Box>
                  );
                })
              ) : (
                <Typography variant="body2" color="grey.500">
                  {t('distill.waitingForLogs')}
                </Typography>
              )}
            </Paper>
          </Box>
        </Box>
      </DialogContent>
    </Dialog>
  );
}

// lib/utils/logger.js
const isElectron = typeof process !== 'undefined' && process.versions && process.versions.electron;

function log(level, ...args) {
  try {
    const message = args.map(arg => (typeof arg === 'object' ? JSON.stringify(arg) : arg)).join(' ');
    if (isElectron) {
      // 在 Electron 环境下，将日志写入文件
      const { ipcRenderer } = require('electron');
      ipcRenderer.send('log', { level, message });
    } else {
      // 在非 Electron 环境下，只输出到控制台
      console[level](...args);
    }
  } catch (error) {
    console.error('Failed to log:', error);
  }
}

export default {
  info: (...args) => log('info', ...args),
  error: (...args) => log('error', ...args),
  warn: (...args) => log('warn', ...args),
  debug: (...args) => log('debug', ...args)
};

/**
 * Builds the GA prompt string.
 * @param {Object} activeGaPair - The currently active GA pair.
 * @returns {String} The constructed GA prompt string.
 */
function buildGaPrompt(activeGaPair = null) {
  if (activeGaPair && activeGaPair.active) {
    return `
## Special Requirements - Genre & Audience Perspective Questioning:
Adjust your questioning approach and question style based on the following genre and audience combination:

**Target Genre**: ${activeGaPair.genre}
**Target Audience**: ${activeGaPair.audience}

Please ensure:
1. The question should fully conform to the style, focus, depth, and other attributes defined by "${activeGaPair.genre}".
2. The question should consider the knowledge level, cognitive characteristics, and potential points of interest of "${activeGaPair.audience}".
3. Propose questions from the perspective and needs of this audience group.
4. Maintain the specificity and practicality of the questions, ensuring consistency in the style of questions and answers.
5. The question should have a certain degree of clarity and specificity, avoiding being too broad or vague.
`;
  }

  return '';
}

/**
 * Question generation prompt template.
 * @param {string} text - The text to be processed.
 * @param {number} number - The number of questions.
 * @param {string} language - The language of the questions.
 * @param {string} globalPrompt - Global prompt for the LLM.
 * @param {string} questionPrompt - Specific prompt for question generation.
 * @param {Object} activeGaPair - The currently active GA pair.
 * @returns {string} The complete prompt for question generation.
 */
module.exports = function getQuestionPrompt({
  text,
  number = Math.floor(text.length / 240),
  language = 'English',
  globalPrompt = '',
  questionPrompt = '',
  activeGaPair = null
}) {
  if (globalPrompt) {
    globalPrompt = `In subsequent tasks, you must strictly follow these rules: ${globalPrompt}`;
  }
  if (questionPrompt) {
    questionPrompt = `- In generating questions, you must strictly follow these rules: ${questionPrompt}`;
  }

  // Build GA pairs related prompts
  const gaPrompt = buildGaPrompt(activeGaPair);

  return `
    # Role Mission
    You are a professional text analysis expert, skilled at extracting key information from complex texts and generating structured data(only generate questions) that can be used for model fine - tuning.
    ${globalPrompt}

    ## Core Task
    Based on the text provided by the user(length: ${text.length} characters), generate no less than ${number} high - quality questions.

    ## Constraints(Important!!!)
    ✔ Must be directly generated based on the text content.
    ✔ Questions should have a clear answer orientation.
    ✔ Should cover different aspects of the text.
    ❌ It is prohibited to generate hypothetical, repetitive, or similar questions.

    ${gaPrompt}

    ## Processing Flow
    1. 【Text Parsing】Process the content in segments, identify key entities and core concepts.
    2. 【Question Generation】Select the best questioning points based on the information density${gaPrompt ? ', and incorporate the specified genre-audience perspective' : ''}
    3. 【Quality Check】Ensure that:
       - The answers to the questions can be found in the original text.
       - The labels are strongly related to the question content.
       - There are no formatting errors.
       ${gaPrompt ? '- Question style matches the specified genre and audience' : ''}

    ## Output Format
    - The JSON array format must be correct.
    - Use English double - quotes for field names.
    - The output JSON array must strictly follow the following structure:
    \`\`\`json
    ["Question 1", "Question 2", "..."]
    \`\`\`

    ## Output Example
    \`\`\`json
    [ "What core elements should an AI ethics framework include?", "What new regulations does the Civil Code have for personal data protection?"]
     \`\`\`

    ## Text to be Processed
    ${text}

    ## Restrictions
    - Must output in the specified JSON format and do not output any other irrelevant content.
    - Generate no less than ${number} high - quality questions.
    - Questions should not be related to the material itself. For example, questions related to the author, chapters, table of contents, etc. are prohibited.
    ${questionPrompt}
    `;
};

'use client';

import { Box, List, ListItem, ListItemIcon, ListItemText, Collapse, IconButton } from '@mui/material';
import FolderIcon from '@mui/icons-material/Folder';
import ArticleIcon from '@mui/icons-material/Article';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import { useTheme } from '@mui/material/styles';

/**
 * 目录结构组件
 * @param {Object} props
 * @param {Array} props.items - 目录项数组
 * @param {Object} props.expandedItems - 展开状态对象
 * @param {Function} props.onToggleItem - 展开/折叠回调
 * @param {number} props.level - 当前层级
 * @param {string} props.parentId - 父级ID
 */
export default function DirectoryView({ items, expandedItems, onToggleItem, level = 0, parentId = '' }) {
  const theme = useTheme();

  if (!items || items.length === 0) return null;

  return (
    <List sx={{ pl: level > 0 ? 2 : 0 }}>
      {items.map((item, index) => {
        const itemId = `${parentId}-${index}`;
        const hasChildren = item.children && item.children.length > 0;
        const isExpanded = expandedItems[itemId] || false;

        return (
          <Box key={itemId}>
            <ListItem
              sx={{
                pl: level * 2,
                borderLeft: level > 0 ? `1px solid ${theme.palette.divider}` : 'none',
                ml: level > 0 ? 1 : 0
              }}
            >
              <ListItemIcon sx={{ minWidth: 36 }}>
                {hasChildren ? <FolderIcon color="primary" /> : <ArticleIcon color="info" />}
              </ListItemIcon>
              <ListItemText
                primary={item.text}
                primaryTypographyProps={{
                  fontWeight: level === 0 ? 'bold' : 'normal',
                  variant: level === 0 ? 'subtitle1' : 'body2'
                }}
              />
              {hasChildren && (
                <IconButton size="small" onClick={() => onToggleItem(itemId)}>
                  {isExpanded ? <ExpandLess /> : <ExpandMore />}
                </IconButton>
              )}
            </ListItem>

            {hasChildren && (
              <Collapse in={isExpanded} timeout="auto" unmountOnExit>
                <DirectoryView
                  items={item.children}
                  expandedItems={expandedItems}
                  onToggleItem={onToggleItem}
                  level={level + 1}
                  parentId={itemId}
                />
              </Collapse>
            )}
          </Box>
        );
      })}
    </List>
  );
}

// 从 LLM 输出中提取 JSON
function extractJsonFromLLMOutput(output) {
  console.log('LLM 输出:', output);
  if (output.trim().startsWith('<think')) {
    output = extractAnswer(output);
  }
  try {
    const json = JSON.parse(output);
    return json;
  } catch {}
  const jsonStart = output.indexOf('```json');
  const jsonEnd = output.lastIndexOf('```');
  if (jsonStart !== -1 && jsonEnd !== -1) {
    const jsonString = output.substring(jsonStart + 7, jsonEnd);
    try {
      const json = JSON.parse(jsonString);
      return json;
    } catch (error) {
      console.error('解析 JSON 时出错:', { error, llmResponse: output });
    }
  } else {
    console.error('模型未按标准格式输出:', output);
    return undefined;
  }
}

function extractThinkChain(text) {
  const startTags = ['<think>', '<thinking>'];
  const endTags = ['</think>', '</thinking>'];
  let startIndex = -1;
  let endIndex = -1;
  let usedStartTag = '';
  let usedEndTag = '';

  for (let i = 0; i < startTags.length; i++) {
    const currentStartIndex = text.indexOf(startTags[i]);
    if (currentStartIndex !== -1) {
      startIndex = currentStartIndex;
      usedStartTag = startTags[i];
      usedEndTag = endTags[i];
      break;
    }
  }

  if (startIndex === -1) {
    return '';
  }

  endIndex = text.indexOf(usedEndTag, startIndex + usedStartTag.length);

  if (endIndex === -1) {
    return '';
  }

  return text.slice(startIndex + usedStartTag.length, endIndex).trim();
}

function extractAnswer(text) {
  const startTags = ['<think>', '<thinking>'];
  const endTags = ['</think>', '</thinking>'];
  for (let i = 0; i < startTags.length; i++) {
    const start = startTags[i];
    const end = endTags[i];
    if (text.includes(start) && text.includes(end)) {
      const partsBefore = text.split(start);
      const partsAfter = partsBefore[1].split(end);
      return (partsBefore[0].trim() + ' ' + partsAfter[1].trim()).trim();
    }
  }
  return text;
}

module.exports = {
  extractJsonFromLLMOutput,
  extractThinkChain,
  extractAnswer
};

'use client';

import { useState } from 'react';
import {
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  Button,
  Radio,
  RadioGroup,
  FormControlLabel,
  FormControl,
  Typography
} from '@mui/material';
import { useTranslation } from 'react-i18next';

/**
 * 领域树操作选择对话框
 * 提供三种选项：修订领域树、重建领域树、不更改领域树
 */
export default function DomainTreeActionDialog({ open, onClose, onConfirm, isFirstUpload, action }) {
  const { t } = useTranslation();
  const [value, setValue] = useState(isFirstUpload ? 'rebuild' : 'revise');

  // 处理选项变更
  const handleChange = event => {
    setValue(event.target.value);
  };

  // 确认选择
  const handleConfirm = () => {
    onConfirm(value);
  };

  // 获取对话框标题
  const getDialogTitle = () => {
    if (isFirstUpload) {
      return t('textSplit.domainTree.firstUploadTitle');
    }
    return action === 'upload' ? t('textSplit.domainTree.uploadTitle') : t('textSplit.domainTree.deleteTitle');
  };

  return (
    <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
      <DialogTitle>{getDialogTitle()}</DialogTitle>
      <DialogContent>
        <FormControl component="fieldset">
          <RadioGroup value={value} onChange={handleChange}>
            {!isFirstUpload && (
              <FormControlLabel
                value="revise"
                control={<Radio />}
                label={
                  <>
                    <Typography variant="subtitle1">{t('textSplit.domainTree.reviseOption')}</Typography>
                    <Typography variant="body2" color="text.secondary">
                      {t('textSplit.domainTree.reviseDesc')}
                    </Typography>
                  </>
                }
              />
            )}
            <FormControlLabel
              value="rebuild"
              control={<Radio />}
              label={
                <>
                  <Typography variant="subtitle1">{t('textSplit.domainTree.rebuildOption')}</Typography>
                  <Typography variant="body2" color="text.secondary">
                    {t('textSplit.domainTree.rebuildDesc')}
                  </Typography>
                </>
              }
            />
            {!isFirstUpload && (
              <FormControlLabel
                value="keep"
                control={<Radio />}
                label={
                  <>
                    <Typography variant="subtitle1">{t('textSplit.domainTree.keepOption')}</Typography>
                    <Typography variant="body2" color="text.secondary">
                      {t('textSplit.domainTree.keepDesc')}
                    </Typography>
                  </>
                }
              />
            )}
          </RadioGroup>
        </FormControl>
      </DialogContent>
      <DialogActions>
        <Button onClick={onClose}>{t('common.cancel')}</Button>
        <Button onClick={handleConfirm} variant="contained" color="primary">
          {t('common.confirm')}
        </Button>
      </DialogActions>
    </Dialog>
  );
}

'use client';

import { Backdrop, Paper, CircularProgress, Typography, Box, LinearProgress } from '@mui/material';

export default function LoadingBackdrop({ open, title, description, progress = null }) {
  return (
    <Backdrop
      sx={{
        color: '#fff',
        zIndex: theme => theme.zIndex.drawer + 1,
        position: 'fixed',
        backdropFilter: 'blur(5px)',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center'
      }}
      open={open}
    >
      <Paper
        elevation={4}
        sx={{
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          p: 4,
          borderRadius: 3,
          bgcolor: 'background.paper',
          minWidth: 280,
          maxWidth: '90%',
          textAlign: 'center',
          boxShadow: '0 8px 32px rgba(0, 0, 0, 0.12)',
          border: '1px solid rgba(0, 0, 0, 0.05)'
        }}
      >
        <CircularProgress
          size={48}
          thickness={4}
          sx={{
            mb: 3,
            color: theme => theme.palette.primary.main
          }}
        />

        <Typography
          variant="h6"
          sx={{
            fontWeight: 600,
            mb: 1,
            textAlign: 'center',
            width: '100%'
          }}
        >
          {title}
        </Typography>

        <Typography
          variant="body1"
          color="text.secondary"
          sx={{
            mb: 2,
            textAlign: 'center',
            width: '100%',
            mx: 'auto'
          }}
        >
          {description}
        </Typography>

        {progress && progress.total > 0 && (
          <Box sx={{ width: '100%', mt: 2 }}>
            <Box
              sx={{
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                mb: 1.5
              }}
            >
              <Typography
                variant="body2"
                color="text.secondary"
                sx={{
                  textAlign: 'center',
                  mb: 0.5
                }}
              >
                {progress.completed}/{progress.total} ({progress.percentage}%)
              </Typography>

              {progress.questionCount > 0 && (
                <Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center' }}>
                  已生成问题数: {progress.questionCount}
                </Typography>
              )}
            </Box>

            <LinearProgress
              variant="determinate"
              value={progress.percentage}
              sx={{
                height: 6,
                borderRadius: 3,
                '& .MuiLinearProgress-bar': {
                  borderRadius: 3
                }
              }}
            />
          </Box>
        )}
      </Paper>
    </Backdrop>
  );
}

import { NextResponse } from 'next/server';
import { batchDeleteQuestions } from '@/lib/db/questions';

// 批量删除问题
export async function DELETE(request) {
  try {
    const body = await request.json();
    const { questionIds } = body;

    // 验证参数
    if (questionIds.length === 0) {
      return NextResponse.json({ error: 'Question ID is required' }, { status: 400 });
    }

    // 删除问题
    await batchDeleteQuestions(questionIds);

    return NextResponse.json({ success: true, message: 'Delete successful' });
  } catch (error) {
    console.error('Delete failed:', String(error));
    return NextResponse.json({ error: error.message || 'Delete failed' }, { status: 500 });
  }
}

/**
 * Markdown文档解析模块
 */

/**
 * 提取文档大纲
 * @param {string} text - Markdown文本
 * @returns {Array} - 提取的大纲数组
 */
function extractOutline(text) {
  const outlineRegex = /^(#{1,6})\s+(.+?)(?:\s*\{#[\w-]+\})?\s*$/gm;
  const outline = [];
  let match;

  while ((match = outlineRegex.exec(text)) !== null) {
    const level = match[1].length;
    const title = match[2].trim();

    outline.push({
      level,
      title,
      position: match.index
    });
  }

  return outline;
}

/**
 * 根据标题分割文档
 * @param {string} text - Markdown文本
 * @param {Array} outline - 文档大纲
 * @returns {Array} - 按标题分割的段落数组
 */
function splitByHeadings(text, outline) {
  if (outline.length === 0) {
    return [
      {
        heading: null,
        level: 0,
        content: text,
        position: 0
      }
    ];
  }

  const sections = [];

  // 添加第一个标题前的内容（如果有）
  if (outline[0].position > 0) {
    const frontMatter = text.substring(0, outline[0].position).trim();
    if (frontMatter.length > 0) {
      sections.push({
        heading: null,
        level: 0,
        content: frontMatter,
        position: 0
      });
    }
  }

  // 分割每个标题的内容
  for (let i = 0; i < outline.length; i++) {
    const current = outline[i];
    const next = i < outline.length - 1 ? outline[i + 1] : null;

    const headingLine = text.substring(current.position).split('\n')[0];
    const startPos = current.position + headingLine.length + 1;
    const endPos = next ? next.position : text.length;

    let content = text.substring(startPos, endPos).trim();

    sections.push({
      heading: current.title,
      level: current.level,
      content: content,
      position: current.position
    });
  }

  return sections;
}

module.exports = {
  extractOutline,
  splitByHeadings
};

import { NextResponse } from 'next/server';

const OllamaClient = require('@/lib/llm/core/providers/ollama');

// 设置为强制动态路由，防止静态生成
export const dynamic = 'force-dynamic';

export async function GET(request) {
  try {
    // 从查询参数中获取 host 和 port
    const { searchParams } = new URL(request.url);
    const host = searchParams.get('host') || '127.0.0.1';
    const port = searchParams.get('port') || '11434';

    // 创建 Ollama API 实例
    const ollama = new OllamaClient({
      endpoint: `http://${host}:${port}/api`
    });
    // 获取模型列表
    const models = await ollama.getModels();
    return NextResponse.json(models);
  } catch (error) {
    // console.error('fetch Ollama models error:', error);
    return NextResponse.json({ error: 'fetch Models failed' }, { status: 500 });
  }
}

/**
 * Incremental domain tree revision prompt template (English version)
 * Used to incrementally adjust the domain tree based on added/deleted literature content
 */
function getLabelReviseEnPrompt({ text, existingTags, deletedContent, newContent, globalPrompt, domainTreePrompt }) {
  const prompt = `${globalPrompt ? globalPrompt : ''}

I need your help to revise an existing domain tree structure to adapt to content changes.
${domainTreePrompt ? domainTreePrompt : ''}

【Existing Domain Tree Structure】
Here is the current domain tree structure (JSON format):
\`\`\`json
${JSON.stringify(existingTags, null, 2)}
\`\`\`

${
  deletedContent
    ? `【Deleted Content】
Here are the table of contents from the deleted literature:
\`\`\`
${deletedContent}
\`\`\`
`
    : ''
}

${
  newContent
    ? `【New Content】
Here are the table of contents from the newly added literature:
\`\`\`
${newContent}
\`\`\`
`
    : ''
}

【All Existing Literature TOC】
Below is an overview of the table of contents from all current literature in the system:
\`\`\`
${text}
\`\`\`

Please analyze the above information and revise the existing domain tree structure according to the following principles:
1. Maintain the overall structure of the domain tree, avoiding large-scale reconstruction
2. For domain tags related to deleted content:
   - If a tag is only related to the deleted content and no supporting content can be found in the existing literature, remove the tag
   - If a tag is also related to other retained content, keep the tag
3. For newly added content:
   - If new content can be classified into existing tags, prioritize using existing tags
   - If new content introduces new domains or concepts not present in the existing tag system, create new tags
4. Each tag must correspond to actual content in the table of contents, do not create empty tags without corresponding content support
5. Ensure that the revised domain tree still has a good hierarchical structure with reasonable parent-child relationships between tags

## Constraints
1. The number of primary domain labels should be between 5 and 10.
2. The number of secondary domain labels ≤ 5 per primary label.
3. There should be at most two classification levels.
4. The classification must be relevant to the original catalog content.
5. The output must conform to the specified JSON format.
6. The names of the labels should not exceed 6 characters.
7. Do not output any content other than the JSON.
8. Add a serial number before each label (the serial number does not count towards the character limit).

Output the complete revised domain tree structure using the JSON format below:

\`\`\`json
[
  {
    "label": "1 Primary Domain Label",
    "child": [
      {"label": "1.1 Secondary Domain Label 1"},
      {"label": "1.2 Secondary Domain Label 2"}
    ]
  },
  {
    "label": "2 Primary Domain Label (No Sub - labels)"
  }
]
\`\`\`

Ensure that your answer only contains the domain tree in JSON format without any explanatory text.`;

  return prompt;
}

module.exports = getLabelReviseEnPrompt;

'use client';

import { Box, Container, Typography, Button, useMediaQuery } from '@mui/material';
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import SearchIcon from '@mui/icons-material/Search';
import { styles } from '@/styles/home';
import { useTheme } from '@mui/material';
import { motion } from 'framer-motion';
import ParticleBackground from './ParticleBackground';
import { useTranslation } from 'react-i18next';

export default function HeroSection({ onCreateProject }) {
  const { t } = useTranslation();
  const theme = useTheme();
  const isMobile = useMediaQuery(theme.breakpoints.down('sm'));

  return (
    <Box sx={{ ...styles.heroSection, ...styles.heroBackground(theme) }}>
      {/* 添加粒子背景 */}
      <ParticleBackground />

      <Box sx={styles.decorativeCircle} />
      <Box sx={styles.decorativeCircleSecond} />

      <Container maxWidth="lg" sx={{ position: 'relative', zIndex: 1 }}>
        <Box
          sx={{
            textAlign: 'center',
            maxWidth: '800px',
            mx: 'auto',
            py: { xs: 5, md: 8 }
          }}
          component={motion.div}
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.8 }}
        >
          <Typography
            variant={isMobile ? 'h3' : 'h1'}
            component="h1"
            fontWeight="bold"
            sx={{
              ...styles.gradientTitle(theme),
              letterSpacing: '-1px',
              mb: 3,
              textShadow: theme.palette.mode === 'dark' ? '0 0 30px rgba(139, 92, 246, 0.3)' : 'none'
            }}
          >
            {t('home.title')}
          </Typography>

          <Typography
            variant={isMobile ? 'body1' : 'h5'}
            component={motion.p}
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            transition={{ delay: 0.3, duration: 0.8 }}
            color="text.secondary"
            paragraph
            sx={{
              maxWidth: '650px',
              mx: 'auto',
              lineHeight: 1.8,
              opacity: 0.9,
              fontSize: { xs: '1rem', md: '1.2rem' },
              fontWeight: 400,
              mb: 4
            }}
          >
            {t('home.subtitle')}
          </Typography>

          <Box
            component={motion.div}
            initial={{ opacity: 0, y: 10 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ delay: 0.5, duration: 0.5 }}
            sx={{
              mt: 6,
              display: 'flex',
              flexDirection: { xs: 'column', sm: 'row' },
              justifyContent: 'center',
              gap: { xs: 2, sm: 3 }
            }}
          >
            <Button
              variant="contained"
              size="large"
              onClick={onCreateProject}
              startIcon={<AddCircleOutlineIcon />}
              sx={{
                ...styles.createButton(theme),
                fontWeight: 600,
                transition: 'all 0.3s ease',
                transform: 'translateY(0)',
                px: 4,
                py: 1.5,
                borderRadius: '12px',
                '&:hover': {
                  transform: 'translateY(-3px)',
                  boxShadow: '0 10px 25px rgba(0, 0, 0, 0.2)'
                }
              }}
            >
              {t('home.createProject')}
            </Button>
            <Button
              variant="contained"
              size="large"
              onClick={() => {
                window.location.href = '/dataset-square';
              }}
              startIcon={<SearchIcon />}
              sx={{
                ...styles.createButton(theme),
                fontWeight: 600,
                transition: 'all 0.3s ease',
                transform: 'translateY(0)',
                px: 4,
                py: 1.5,
                borderRadius: '12px',
                '&:hover': {
                  transform: 'translateY(-3px)',
                  boxShadow: '0 10px 25px rgba(0, 0, 0, 0.2)'
                }
              }}
            >
              {t('home.searchDataset')}
            </Button>
          </Box>
        </Box>
      </Container>
    </Box>
  );
}

'use client';

import { useState } from 'react';
import { Box, Typography, Button, TextField, IconButton, Switch, FormControlLabel } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import ReactMarkdown from 'react-markdown';
import { useTranslation } from 'react-i18next';
import { useTheme } from '@mui/material/styles';

/**
 * 可编辑字段组件，支持 Markdown 和原始文本两种展示方式
 */
export default function EditableField({
  label,
  value,
  multiline = true,
  editing,
  onEdit,
  onChange,
  onSave,
  onCancel,
  onOptimize,
  tokenCount
}) {
  const { t } = useTranslation();
  const theme = useTheme();
  const [useMarkdown, setUseMarkdown] = useState(false);

  const toggleMarkdown = () => {
    setUseMarkdown(!useMarkdown);
  };

  return (
    <Box sx={{ mb: 3 }}>
      <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
        <Typography variant="subtitle1" color="text.secondary" sx={{ mr: 1 }}>
          {label}
        </Typography>
        {!editing && value && (
          <>
            {/* 字符数标签 */}
            <Box
              sx={{
                display: 'inline-flex',
                alignItems: 'center',
                borderRadius: '12px',
                bgcolor: 'info.50',
                color: 'info.main',
                px: 1,
                py: 0.25,
                fontSize: '0.75rem',
                fontWeight: 500,
                border: '1px solid',
                borderColor: 'info.100',
                mr: 1
              }}
            >
              {value.length} Characters
            </Box>

            {/* Token 标签 */}
            {tokenCount > 0 && (
              <Box
                sx={{
                  display: 'inline-flex',
                  alignItems: 'center',
                  borderRadius: '12px',
                  bgcolor: 'primary.50',
                  color: 'primary.main',
                  px: 1,
                  py: 0.25,
                  fontSize: '0.75rem',
                  fontWeight: 500,
                  border: '1px solid',
                  borderColor: 'primary.100',
                  mr: 1
                }}
              >
                {tokenCount} Tokens
              </Box>
            )}
          </>
        )}
        {!editing && (
          <>
            <IconButton size="small" onClick={onEdit}>
              <EditIcon fontSize="small" />
            </IconButton>
            {onOptimize && (
              <IconButton size="small" onClick={onOptimize} sx={{ ml: 0.5 }}>
                <AutoFixHighIcon fontSize="small" />
              </IconButton>
            )}
            <FormControlLabel
              control={<Switch size="small" checked={useMarkdown} onChange={toggleMarkdown} sx={{ ml: 1 }} />}
              label={<Typography variant="caption">{useMarkdown ? 'Markdown' : 'Text'}</Typography>}
              sx={{ ml: 1 }}
            />
          </>
        )}
      </Box>
      {editing ? (
        <>
          <TextField
            fullWidth
            multiline={multiline}
            rows={10}
            value={value}
            onChange={onChange}
            variant="outlined"
            sx={{
              mb: 2,
              backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)'
            }}
          />
          <Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
            <Button variant="outlined" onClick={onCancel}>
              {t('common.cancel')}
            </Button>
            <Button variant="contained" onClick={onSave}>
              {t('common.save')}
            </Button>
          </Box>
        </>
      ) : (
        <Box
          sx={{
            whiteSpace: 'pre-wrap',
            p: 2,
            borderRadius: 1,
            backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)',
            '& img': {
              maxWidth: '100%'
            },
            '& pre': {
              backgroundColor: theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.05)',
              padding: '8px',
              borderRadius: '4px',
              overflowX: 'auto'
            },
            '& code': {
              backgroundColor: theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.05)',
              padding: '2px 4px',
              borderRadius: '4px',
              fontFamily: 'monospace'
            }
          }}
        >
          {value ? (
            useMarkdown ? (
              <ReactMarkdown>{value}</ReactMarkdown>
            ) : (
              <Typography variant="body1">{value}</Typography>
            )
          ) : (
            <Typography variant="body2" color="text.secondary">
              {t('common.noData')}
            </Typography>
          )}
        </Box>
      )}
    </Box>
  );
}

import { createHash } from 'crypto';
import { createReadStream } from 'fs';

export async function getFileMD5(filePath) {
  return new Promise((resolve, reject) => {
    const hash = createHash('md5');
    const stream = createReadStream(filePath);

    stream.on('data', chunk => hash.update(chunk));
    stream.on('end', () => resolve(hash.digest('hex')));
    stream.on('error', reject);
  });
}

export function filterDomainTree(tree = []) {
  for (let i = 0; i < tree.length; i++) {
    const { child } = tree[i];
    delete tree[i].id;
    delete tree[i].projectId;
    delete tree[i].parentId;
    delete tree[i].questionCount;
    filterDomainTree(child);
  }
  return tree;
}

'use client';

/**
 * 按照标签前面的序号对标签进行排序
 * @param {Array} tags - 标签数组
 * @returns {Array} 排序后的标签数组
 */
export const sortTagsByNumber = tags => {
  return [...tags].sort((a, b) => {
    // 提取标签前面的序号
    const getNumberPrefix = label => {
      // 匹配形如 1, 1.1, 1.1.2 的序号
      const match = label.match(/^([\d.]+)\s/);
      if (match) {
        return match[1]; // 返回完整的序号字符串，如 "1.10"
      }
      return null; // 没有序号
    };

    const aPrefix = getNumberPrefix(a.label);
    const bPrefix = getNumberPrefix(b.label);

    // 如果两个标签都有序号，按序号比较
    if (aPrefix && bPrefix) {
      // 将序号分解为数组，然后按数值比较
      const aParts = aPrefix.split('.').map(num => parseInt(num, 10));
      const bParts = bPrefix.split('.').map(num => parseInt(num, 10));

      // 比较序号数组
      for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
        if (aParts[i] !== bParts[i]) {
          return aParts[i] - bParts[i]; // 数值比较，确保 1.2 排在 1.10 前面
        }
      }
      // 如果前面的数字都相同，则较短的序号在前
      return aParts.length - bParts.length;
    }
    // 如果只有一个标签有序号，则有序号的在前
    else if (aPrefix) {
      return -1;
    } else if (bPrefix) {
      return 1;
    }
    // 如果都没有序号，则按原来的字母序排序
    else {
      return a.label.localeCompare(b.label, 'zh-CN');
    }
  });
};

/**
 * 获取标签的完整路径
 * @param {Object} tag - 标签对象
 * @param {Array} allTags - 所有标签数组
 * @returns {string} 标签路径，如 "标签1 > 标签2 > 标签3"
 */
export const getTagPath = (tag, allTags) => {
  if (!tag) return '';

  const findPath = (currentTag, path = []) => {
    const newPath = [currentTag.label, ...path];

    if (!currentTag.parentId) return newPath;

    const parentTag = allTags.find(t => t.id === currentTag.parentId);
    if (!parentTag) return newPath;

    return findPath(parentTag, newPath);
  };

  return findPath(tag).join(' > ');
};

'use client';

import React from 'react';
import { Chip, CircularProgress, Box } from '@mui/material';
import { useTranslation } from 'react-i18next';

// 任务状态显示组件
export default function TaskStatusChip({ status }) {
  const { t } = useTranslation();

  // 状态映射配置
  const STATUS_CONFIG = {
    0: {
      label: t('tasks.status.processing'),
      color: 'warning',
      loading: true
    },
    1: {
      label: t('tasks.status.completed'),
      color: 'success'
    },
    2: {
      label: t('tasks.status.failed'),
      color: 'error'
    },
    3: {
      label: t('tasks.status.aborted'),
      color: 'default'
    }
  };

  const statusInfo = STATUS_CONFIG[status] || {
    label: t('tasks.status.unknown'),
    color: 'default'
  };

  // 处理中状态显示加载动画
  if (status === 0) {
    return (
      <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
        <CircularProgress size={16} color="warning" />
        <Chip label={statusInfo.label} color={statusInfo.color} size="small" />
      </Box>
    );
  }

  return <Chip label={statusInfo.label} color={statusInfo.color} size="small" />;
}

'use client';

import {
  Dialog,
  DialogTitle,
  DialogContent,
  DialogContentText,
  DialogActions,
  Button,
  Typography,
  Box,
  Alert
} from '@mui/material';
import { useTranslation } from 'react-i18next';

export default function DeleteConfirmDialog({ open, fileName, onClose, onConfirm }) {
  const { t } = useTranslation();
  return (
    <Dialog
      open={open}
      onClose={onClose}
      aria-labelledby="delete-dialog-title"
      aria-describedby="delete-dialog-description"
      maxWidth="sm"
      fullWidth
    >
      <DialogTitle id="delete-dialog-title">
        {t('common.confirmDelete')}「{fileName}」?
      </DialogTitle>
      <DialogContent>
        <DialogContentText id="delete-dialog-description">{t('common.confirmDeleteDescription')}</DialogContentText>

        <Alert severity="warning" sx={{ my: 2 }}>
          <Typography variant="body2" component="div" fontWeight="medium">
            {t('textSplit.deleteFileWarning')}
          </Typography>
          <Box sx={{ mt: 1 }}>
            <Typography variant="body2" component="div">
              • {t('textSplit.deleteFileWarningChunks')}
            </Typography>
            <Typography variant="body2" component="div">
              • {t('textSplit.deleteFileWarningQuestions')}
            </Typography>
            <Typography variant="body2" component="div">
              • {t('textSplit.deleteFileWarningDatasets')}
            </Typography>
          </Box>
        </Alert>
      </DialogContent>
      <DialogActions>
        <Button onClick={onClose} color="primary">
          {t('common.cancel')}
        </Button>
        <Button onClick={onConfirm} color="error" variant="contained">
          {t('common.delete')}
        </Button>
      </DialogActions>
    </Dialog>
  );
}

module.exports = function getLabelPrompt({ text, globalPrompt, domainTreePrompt }) {
  if (globalPrompt) {
    globalPrompt = `- In subsequent tasks, you must follow this rule: ${globalPrompt}`;
  }
  if (domainTreePrompt) {
    domainTreePrompt = `- In generating labels, you must follow this rule: ${domainTreePrompt}`;
  }
  return `
# Role: Domain Classification Expert & Knowledge Graph Expert
- Description: As a senior domain classification expert and knowledge graph expert, you are skilled at extracting core themes from text content, constructing classification systems, and performing knowledge categorization and labeling.
${globalPrompt}

## Skills:
1. Proficient in text theme analysis and keyword extraction.
2. Good at constructing hierarchical knowledge systems.
3. Skilled in domain classification methodologies.
4. Capable of building knowledge graphs.
5. Proficient in JSON data structures.

## Goals:
1. Analyze the content of the book catalog.
2. Identify core themes and key domains.
3. Construct a two - level classification system.
4. Ensure the classification logic is reasonable.
5. Generate a standardized JSON output.

## Workflow:
1. Carefully read the entire content of the book catalog.
2. Extract key themes and core concepts.
3. Group and categorize the themes.
4. Construct primary domain labels (ensure no more than 10).
5. Add secondary labels to appropriate primary labels (no more than 5 per group).
6. Check the rationality of the classification logic.
7. Generate a JSON output that conforms to the format.

## Catalog to be analyzed
    ${text}

## Constraints
1. The number of primary domain labels should be between 5 and 10.
2. The number of secondary domain labels ≤ 5 per primary label.
3. There should be at most two classification levels.
4. The classification must be relevant to the original catalog content.
5. The output must conform to the specified JSON format.
6. The names of the labels should not exceed 6 characters.
7. Do not output any content other than the JSON.
8. Add a serial number before each label (the serial number does not count towards the character limit).
9. Use English
${domainTreePrompt}

## OutputFormat:
\`\`\`json
[
  {
    "label": "1 Primary Domain Label",
    "child": [
      {"label": "1.1 Secondary Domain Label 1"},
      {"label": "1.2 Secondary Domain Label 2"}
    ]
  },
  {
    "label": "2 Primary Domain Label (No Sub - labels)"
  }
]
\`\`\`
    `;
};

import { NextResponse } from 'next/server';
import { getLlmProviders } from '@/lib/db/llm-providers';

// 获取 LLM 提供商数据
export async function GET() {
  try {
    const result = await getLlmProviders();
    return NextResponse.json(result);
  } catch (error) {
    console.error('Database query error:', String(error));
    return NextResponse.json({ error: 'Database query failed' }, { status: 500 });
  }
}

module.exports = function getAnswerPrompt({
  text,
  question,
  language = 'English',
  globalPrompt = '',
  answerPrompt = ''
}) {
  if (globalPrompt) {
    globalPrompt = `In subsequent tasks, you must strictly follow these rules: ${globalPrompt}`;
  }
  if (answerPrompt) {
    answerPrompt = `In generating answers, you must strictly follow these rules: ${answerPrompt}`;
  }

  return `
# Role: Fine-tuning Dataset Generation Expert
## Profile:
- Description: You are an expert in generating fine-tuning datasets, skilled at generating accurate answers to questions from the given content, ensuring the accuracy and relevance of the answers.
${globalPrompt}

## Skills:
1. The answer must be based on the given content.
2. The answer must be accurate and not fabricated.
3. The answer must be relevant to the question.
4. The answer must be logical.

## Workflow:
1. Take a deep breath and work on this problem step-by-step.
2. First, analyze the given file content.
3. Then, extract key information from the content.
4. Next, generate an accurate answer related to the question.
5. Finally, ensure the accuracy and relevance of the answer.

## Reference Content:
${text}

## Question
${question}

## Constrains:
1. The answer must be based on the given content.
2. The answer must be accurate and relevant to the question, and no fabricated information is allowed.
3. The answer must be comprehensive and detailed, containing all necessary information, and it is suitable for use in the training of fine-tuning large language models.
    ${answerPrompt}
`;
};

'use client';

import { createTheme, ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider as NextThemeProvider, useTheme } from 'next-themes';
import { useEffect, useState } from 'react';

// 导入字体
import '@fontsource/inter/300.css';
import '@fontsource/inter/400.css';
import '@fontsource/inter/500.css';
import '@fontsource/inter/600.css';
import '@fontsource/inter/700.css';
import '@fontsource/jetbrains-mono/400.css';
import '@fontsource/jetbrains-mono/500.css';

// 创建主题配置
const getTheme = mode => {
  // 主色调
  const mainBlue = '#2A5CAA';
  const darkGray = '#2D2D2D';

  // 辅助色 - 数据可视化色谱
  const dataVizColors = [
    '#6366F1', // 紫蓝色
    '#10B981', // 绿色
    '#F59E0B', // 琥珀色
    '#EC4899', // 粉色
    '#8B5CF6', // 紫色
    '#3B82F6' // 蓝色
  ];

  // 状态色
  const successColor = '#10B981'; // 翡翠绿
  const warningColor = '#F59E0B'; // 琥珀色
  const errorColor = '#EF4444'; // 珊瑚红

  // 渐变色
  const gradientPrimary = 'linear-gradient(90deg, #2A5CAA 0%, #8B5CF6 100%)';

  // 根据模式调整颜色
  return createTheme({
    palette: {
      mode,
      primary: {
        main: mainBlue,
        dark: '#1E4785',
        light: '#4878C6',
        contrastText: '#FFFFFF'
      },
      secondary: {
        main: '#8B5CF6',
        dark: '#7039F2',
        light: '#A78BFA',
        contrastText: '#FFFFFF'
      },
      error: {
        main: errorColor,
        dark: '#DC2626',
        light: '#F87171'
      },
      warning: {
        main: warningColor,
        dark: '#D97706',
        light: '#FBBF24'
      },
      success: {
        main: successColor,
        dark: '#059669',
        light: '#34D399'
      },
      background: {
        default: mode === 'dark' ? '#121212' : '#F8F9FA',
        paper: mode === 'dark' ? '#1E1E1E' : '#FFFFFF',
        subtle: mode === 'dark' ? '#2A2A2A' : '#F3F4F6'
      },
      text: {
        primary: mode === 'dark' ? '#F3F4F6' : darkGray,
        secondary: mode === 'dark' ? '#9CA3AF' : '#6B7280',
        disabled: mode === 'dark' ? '#4B5563' : '#9CA3AF'
      },
      divider: mode === 'dark' ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.12)',
      dataViz: dataVizColors,
      gradient: {
        primary: gradientPrimary
      }
    },
    typography: {
      fontFamily:
        '"Inter", "HarmonyOS Sans", "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
      fontSize: 14,
      fontWeightLight: 300,
      fontWeightRegular: 400,
      fontWeightMedium: 500,
      fontWeightBold: 600,
      h1: {
        fontSize: '2rem', // 32px
        fontWeight: 600,
        lineHeight: 1.2,
        letterSpacing: '-0.01em'
      },
      h2: {
        fontSize: '1.5rem', // 24px
        fontWeight: 600,
        lineHeight: 1.3,
        letterSpacing: '-0.005em'
      },
      h3: {
        fontSize: '1.25rem', // 20px
        fontWeight: 600,
        lineHeight: 1.4
      },
      h4: {
        fontSize: '1.125rem', // 18px
        fontWeight: 600,
        lineHeight: 1.4
      },
      h5: {
        fontSize: '1rem', // 16px
        fontWeight: 600,
        lineHeight: 1.5
      },
      h6: {
        fontSize: '0.875rem', // 14px
        fontWeight: 600,
        lineHeight: 1.5
      },
      body1: {
        fontSize: '1rem', // 16px
        lineHeight: 1.5
      },
      body2: {
        fontSize: '0.875rem', // 14px
        lineHeight: 1.5
      },
      caption: {
        fontSize: '0.75rem', // 12px
        lineHeight: 1.5
      },
      code: {
        fontFamily: '"JetBrains Mono", monospace',
        fontSize: '0.875rem'
      }
    },
    shape: {
      borderRadius: 8
    },
    spacing: 8, // 基础间距单位为8px
    components: {
      MuiCssBaseline: {
        styleOverrides: {
          body: {
            scrollbarWidth: 'thin',
            scrollbarColor: mode === 'dark' ? '#4B5563 transparent' : '#9CA3AF transparent',
            '&::-webkit-scrollbar': {
              width: '8px',
              height: '8px'
            },
            '&::-webkit-scrollbar-track': {
              background: 'transparent'
            },
            '&::-webkit-scrollbar-thumb': {
              background: mode === 'dark' ? '#4B5563' : '#9CA3AF',
              borderRadius: '4px'
            }
          },
          // 确保代码块使用 JetBrains Mono 字体
          'code, pre': {
            fontFamily: '"JetBrains Mono", monospace'
          },
          // 自定义渐变文本的通用样式
          '.gradient-text': {
            background: gradientPrimary,
            WebkitBackgroundClip: 'text',
            WebkitTextFillColor: 'transparent',
            backgroundClip: 'text',
            textFillColor: 'transparent'
          }
        }
      },
      MuiButton: {
        styleOverrides: {
          root: {
            textTransform: 'none',
            fontWeight: 500,
            borderRadius: '8px',
            padding: '6px 16px'
          },
          contained: {
            boxShadow: 'none',
            '&:hover': {
              boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)'
            }
          },
          containedPrimary: {
            background: mainBlue,
            '&:hover': {
              backgroundColor: '#1E4785'
            }
          },
          containedSecondary: {
            background: '#8B5CF6',
            '&:hover': {
              backgroundColor: '#7039F2'
            }
          },
          outlined: {
            borderWidth: '1.5px',
            '&:hover': {
              borderWidth: '1.5px'
            }
          }
        }
      },
      MuiAppBar: {
        styleOverrides: {
          root: {
            boxShadow: 'none',
            background: mode === 'dark' ? '#1A1A1A' : mainBlue
          }
        }
      },
      MuiCard: {
        styleOverrides: {
          root: {
            borderRadius: '12px',
            boxShadow: mode === 'dark' ? '0px 4px 8px rgba(0, 0, 0, 0.4)' : '0px 4px 8px rgba(0, 0, 0, 0.05)'
          }
        }
      },
      MuiPaper: {
        styleOverrides: {
          root: {
            borderRadius: '12px'
          }
        }
      },
      MuiChip: {
        styleOverrides: {
          root: {
            borderRadius: '6px',
            fontWeight: 500
          }
        }
      },
      MuiTableHead: {
        styleOverrides: {
          root: {
            '& .MuiTableCell-head': {
              fontWeight: 600,
              backgroundColor: mode === 'dark' ? '#2A2A2A' : '#F3F4F6'
            }
          }
        }
      },
      MuiTabs: {
        styleOverrides: {
          indicator: {
            height: '3px',
            borderRadius: '3px 3px 0 0'
          }
        }
      },
      MuiTab: {
        styleOverrides: {
          root: {
            textTransform: 'none',
            fontWeight: 500,
            '&.Mui-selected': {
              fontWeight: 600
            }
          }
        }
      },
      MuiListItemButton: {
        styleOverrides: {
          root: {
            borderRadius: '8px'
          }
        }
      },
      MuiDialogTitle: {
        styleOverrides: {
          root: {
            fontSize: '1.25rem',
            fontWeight: 600
          }
        }
      }
    }
  });
};

export default function ThemeRegistry({ children }) {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return null;
  }

  return (
    <NextThemeProvider attribute="class" defaultTheme="system" enableSystem>
      <InnerThemeRegistry>{children}</InnerThemeRegistry>
    </NextThemeProvider>
  );
}

function InnerThemeRegistry({ children }) {
  const { resolvedTheme } = useTheme();
  const theme = getTheme(resolvedTheme === 'dark' ? 'dark' : 'light');

  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      {children}
    </ThemeProvider>
  );
}

const { BrowserWindow, shell } = require('electron');
const path = require('path');
const url = require('url');
const { getAppVersion } = require('../util');

let mainWindow;

/**
 * 创建主窗口
 * @param {boolean} isDev 是否为开发环境
 * @param {number} port 服务端口
 * @returns {BrowserWindow} 创建的主窗口
 */
function createWindow(isDev, port) {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    show: false,
    frame: true,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      preload: path.join(__dirname, '..', 'preload.js')
    },
    icon: path.join(__dirname, '../../public/imgs/logo.ico')
  });

  // 设置窗口标题
  mainWindow.setTitle(`Easy Dataset v${getAppVersion()}`);
  const loadingPath = url.format({
    pathname: path.join(__dirname, '..', 'loading.html'),
    protocol: 'file:',
    slashes: true
  });

  // 加载 loading 页面时使用专门的 preload 脚本
  mainWindow.webContents.on('did-finish-load', () => {
    mainWindow.show();
  });

  mainWindow.loadURL(loadingPath);

  // 处理窗口导航事件，将外部链接在浏览器中打开
  mainWindow.webContents.on('will-navigate', (event, navigationUrl) => {
    // 解析当前 URL 和导航 URL
    const parsedUrl = new URL(navigationUrl);
    const currentHostname = isDev ? 'localhost' : 'localhost';
    const currentPort = port.toString();

    // 检查是否是外部链接
    if (parsedUrl.hostname !== currentHostname || (parsedUrl.port !== currentPort && parsedUrl.port !== '')) {
      event.preventDefault();
      shell.openExternal(navigationUrl);
    }
  });

  // 处理新窗口打开请求，将外部链接在浏览器中打开
  mainWindow.webContents.setWindowOpenHandler(({ url: navigationUrl }) => {
    // 解析导航 URL
    const parsedUrl = new URL(navigationUrl);
    const currentHostname = isDev ? 'localhost' : 'localhost';
    const currentPort = port.toString();

    // 检查是否是外部链接
    if (parsedUrl.hostname !== currentHostname || (parsedUrl.port !== currentPort && parsedUrl.port !== '')) {
      shell.openExternal(navigationUrl);
      return { action: 'deny' };
    }
    return { action: 'allow' };
  });

  mainWindow.on('closed', () => {
    mainWindow = null;
  });

  mainWindow.maximize();

  return mainWindow;
}

/**
 * 加载应用URL
 * @param {string} appUrl 应用URL
 */
function loadAppUrl(appUrl) {
  if (mainWindow) {
    mainWindow.loadURL(appUrl);
  }
}

/**
 * 在开发环境中打开开发者工具
 */
function openDevTools() {
  if (mainWindow) {
    mainWindow.webContents.openDevTools();
  }
}

/**
 * 获取主窗口
 * @returns {BrowserWindow} 主窗口
 */
function getMainWindow() {
  return mainWindow;
}

module.exports = {
  createWindow,
  loadAppUrl,
  openDevTools,
  getMainWindow
};

function removeLeadingNumber(label) {
  // 正则说明：
  // ^\d+       匹配开头的一个或多个数字
  // (?:\.\d+)* 匹配零个或多个「点+数字」的组合（非捕获组）
  // \s+        匹配序号后的一个或多个空格（确保序号与内容有空格分隔）
  const numberPrefixRegex = /^\d+(?:\.\d+)*\s+/;
  // 仅当匹配到数字开头的序号时才替换，否则返回原标签
  return label.replace(numberPrefixRegex, '');
}

/**
 * 根据标签构造问题的提示词
 * @param {string} tagPath - 标签链路，例如 "体育->足球->足球先生"
 * @param {string} currentTag - 当前子标签，例如 "足球先生"
 * @param {number} count - 希望生成问题的数量，例如：10
 * @param {Array<string>} existingQuestions - 当前标签已经生成的问题（避免重复）
 * @param {string} globalPrompt - 项目全局提示词
 * @returns {string} 提示词
 */
export function distillQuestionsEnPrompt(tagPath, currentTag, count = 10, existingQuestions = [], globalPrompt = '') {
  currentTag = removeLeadingNumber(currentTag);
  const existingQuestionsText =
    existingQuestions.length > 0
      ? `Existing questions include:\n${existingQuestions.map(q => `- ${q}`).join('\n')}\nPlease do not generate questions that are repetitive or highly similar to these.`
      : '';

  // 构建全局提示词部分
  const globalPromptText = globalPrompt ? `You must adhere to this requirement: ${globalPrompt}` : '';

  return `
You are a professional knowledge question generation assistant, proficient in the field of ${currentTag}. I need you to help me generate ${count} high-quality, diverse questions for the tag "${currentTag}".

The complete tag path is: ${tagPath}

Please follow these rules:
${globalPromptText}
1. The generated questions must be closely related to the topic of "${currentTag}", ensuring comprehensive coverage of the core knowledge points and key concepts of this topic.
2. Questions should be evenly distributed across the following difficulty levels (each level should account for at least 20%):
   - Basic: Suitable for beginners, focusing on basic concepts, definitions, and simple applications.
   - Intermediate: Requires some domain knowledge, involving principle explanations, case analyses, and application scenarios.
   - Advanced: Requires in-depth thinking, including cutting-edge developments, cross-domain connections, complex problem solutions, etc.

3. Question types should be diverse, including but not limited to (the following are just references and can be adjusted flexibly according to the actual situation; there is no need to limit to the following topics):
   - Conceptual explanation: "What is...", "How to define..."
   - Principle analysis: "Why...", "How to explain..."
   - Comparison and contrast: "What is the difference between... and...", "What are the advantages of... compared to..."
   - Application practice: "How to apply... to solve...", "What is the best practice for..."
   - Development trends: "What is the future development direction of...", "What challenges does... face?"
   - Case analysis: "Please analyze... in the case of..."
   - Thought-provoking: "What would happen if...", "How to evaluate..."

4. Question phrasing should be clear, accurate, and professional. Avoid the following:
   - Avoid vague or overly broad phrasing.
   - Avoid closed-ended questions that can be answered with "yes/no".
   - Avoid questions containing misleading assumptions.
   - Avoid repetitive or highly similar questions.

5. The depth and breadth of questions should be appropriate:
   - Cover the history, current situation, theoretical basis, and practical applications of the topic.
   - Include mainstream views and controversial topics in the field.
   - Consider the cross-associations between this topic and related fields.
   - Focus on emerging technologies, methods, or trends in this field.

${existingQuestionsText}

Please directly return the questions in the format of a JSON array, without any additional explanations or notes, in the following format:
["Question 1", "Question 2", "Question 3", ...]

Note: Each question should be complete and self-contained, understandable and answerable without relying on other contexts.
`;
}

/**
 * Prompt for constructing sub-tags based on parent tag
 * @param {string} tagPath - Tag chain, e.g., "Knowledge Base->Sports"
 * @param {string} parentTag - Parent tag name, e.g., "Sports"
 * @param {Array<string>} existingTags - Existing sub-tags under this parent tag (to avoid duplicates), e.g., ["Football", "Table Tennis"]
 * @param {number} count - Number of sub-tags to generate, e.g.: 10
 * @param {string} globalPrompt - Project-wide global prompt
 * @returns {string} Prompt
 */
export function distillTagsEnPrompt(tagPath, parentTag, existingTags = [], count = 10, globalPrompt = '') {
  const existingTagsText =
    existingTags.length > 0
      ? `Existing sub-tags include: ${existingTags.join('、')}. Please do not generate duplicate tags.`
      : '';

  // Build the global prompt section
  const globalPromptText = globalPrompt ? `You must follow this requirement: ${globalPrompt}` : '';

  return `
You are a professional knowledge tag generation assistant. I need you to generate ${count} sub-tags for the parent tag "${parentTag}".

The full tag chain is: ${tagPath || parentTag}

Please follow these rules:
${globalPromptText}
1. Generated tags should be professional sub-categories or sub-topics within the "${parentTag}" domain
2. Each tag should be concise and clear, typically 2-6 characters
3. Tags should be clearly distinguishable, covering different aspects
4. Tags should be nouns or noun phrases; avoid verbs or adjectives
5. Tags should be practical and serve as a basis for question generation
6. Tags should have explicit numbering. If the parent tag is numbered (e.g., 1 Automobiles), sub-tags should be 1.1 Car Brands, 1.2 Car Models, 1.3 Car Prices, etc.
7. If the parent tag is unnumbered (e.g., "Automobiles"), indicating top-level tag generation, sub-tags should be 1 Car Brands 2 Car Models 3 Car Prices, etc.

${existingTagsText}

Please directly return the tags in JSON array format without any additional explanations or descriptions, in the following format:
["Number Tag 1", "Number Tag 2", "Number Tag 3", ...]
`;
}

module.exports = function reTitlePromptEn() {
  return `
    You are a professional text structuring assistant specializing in analyzing and optimizing the hierarchical
    structure of Markdown document titles based on prefix rules and semantic analysis. Please process the Markdown titles
    I provide according to the following requirements:
    ## Task Description
    Adjust the correct hierarchical relationships of titles based on the actual meaning of the Markdown article titles and the prefix characteristics of the titles. The specific requirements are as follows:

    1. Titles with the same prefix format are generally at the same level ({title} represents the actual title content):
        For example:
        - Titles starting with pure number prefixes: \`1 {title}\`, \`2 {title}\`, \`3 {title}\`, \`4 {title}\`, \`5 {title}\`, etc.
        - Titles starting with Roman numeral prefixes: \`I {title}\`, \`II {title}\`, \`III {title}\`, \`IV {title}\`, \`V {title}\`, etc.
        - Titles starting with decimal-separated array prefixes: \`1.1 {title}\`, \`1.2 {title}\`, \`1.3 {title}\`, ..., \`2.1 {title}\`, \`2.2 {title}\`, etc.

    2. Correctly nest sub-titles under parent titles (e.g., \`1.1 {title}\` should be a sub-title of \`1 {title}\`).
    3. Remove titles unrelated to the content of the article.
    4. Keep the content of the output titles identical to the input.
    5. Ensure no content is missing.
    6. For Chinese literature with English article titles, the English titles can be omitted.

    ## Input and Output Format
    - Input: Markdown title structure with incorrect hierarchical relationships.
    - Output: Corrected standard Markdown title hierarchical structure.

    ## Processing Principles
    1. Strictly determine the hierarchical relationship based on the semantic meaning of the titles.
    2. Adjust only the hierarchy without modifying the original title text.
    3. Directly remove unrelated titles without retaining placeholders.
    4. Titles with the same prefix rules must be at the same level; they cannot be partially at one level and partially at another.

    ## Output Requirements
    Please return the corrected complete title structure within a code block, formatted as follows:

    Expected Output:
        \`\`\`markdown

        \`\`\`

    Please process the following data:
      `;
};

'use client';

import React from 'react';
import {
  Box,
  FormControl,
  InputLabel,
  Select,
  MenuItem,
  OutlinedInput,
  IconButton,
  Tooltip,
  CircularProgress
} from '@mui/material';
import RefreshIcon from '@mui/icons-material/Refresh';
import { useTranslation } from 'react-i18next';

// 任务筛选组件
export default function TaskFilters({ statusFilter, setStatusFilter, typeFilter, setTypeFilter, loading, onRefresh }) {
  const { t } = useTranslation();

  return (
    <Box sx={{ display: 'flex', gap: 2 }}>
      {/* 状态筛选 */}
      <FormControl size="small" sx={{ minWidth: 120 }}>
        <InputLabel>{t('tasks.filters.status')}</InputLabel>
        <Select
          value={statusFilter}
          onChange={e => setStatusFilter(e.target.value)}
          input={<OutlinedInput label={t('tasks.filters.status')} />}
        >
          <MenuItem value="all">{t('datasets.filterAll')}</MenuItem>
          <MenuItem value="0">{t('tasks.status.processing')}</MenuItem>
          <MenuItem value="1">{t('tasks.status.completed')}</MenuItem>
          <MenuItem value="2">{t('tasks.status.failed')}</MenuItem>
          <MenuItem value="3">{t('tasks.status.aborted')}</MenuItem>
        </Select>
      </FormControl>

      {/* 类型筛选 */}
      <FormControl size="small" sx={{ minWidth: 120 }}>
        <InputLabel>{t('tasks.filters.type')}</InputLabel>
        <Select
          value={typeFilter}
          onChange={e => setTypeFilter(e.target.value)}
          input={<OutlinedInput label={t('tasks.filters.type')} />}
        >
          <MenuItem value="all">{t('datasets.filterAll')}</MenuItem>
          <MenuItem value="text-processing">{t('tasks.types.text-processing')}</MenuItem>
          <MenuItem value="question-generation">{t('tasks.types.question-generation')}</MenuItem>
          <MenuItem value="answer-generation">{t('tasks.types.answer-generation')}</MenuItem>
          <MenuItem value="data-distillation">{t('tasks.types.data-distillation')}</MenuItem>
          <MenuItem value="pdf-processing">{t('tasks.types.pdf-processing')}</MenuItem>
        </Select>
      </FormControl>

      {/* 刷新按钮 */}
      <Tooltip title={t('tasks.actions.refresh')}>
        <IconButton onClick={onRefresh} disabled={loading}>
          {loading ? <CircularProgress size={20} /> : <RefreshIcon />}
        </IconButton>
      </Tooltip>
    </Box>
  );
}

'use client';

import React from 'react';
import { Grid, Button, Divider, FormControl, InputLabel, Select, MenuItem } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import { useTheme } from '@mui/material/styles';
import ModelSelector from './ModelSelector';
import { playgroundStyles } from '@/styles/playground';
import { useTranslation } from 'react-i18next';

const PlaygroundHeader = ({
  availableModels,
  selectedModels,
  handleModelSelection,
  handleClearConversations,
  conversations,
  outputMode,
  handleOutputModeChange
}) => {
  const theme = useTheme();
  const styles = playgroundStyles(theme);
  const { t } = useTranslation();

  const isClearDisabled = selectedModels.length === 0 || Object.values(conversations).every(conv => conv.length === 0);

  return (
    <>
      <Grid container spacing={2} sx={styles.controlsContainer}>
        <Grid item xs={12} md={6}>
          <ModelSelector models={availableModels} selectedModels={selectedModels} onChange={handleModelSelection} />
        </Grid>
        <Grid item xs={12} md={3}>
          <FormControl fullWidth>
            <InputLabel id="output-mode-label">{t('playground.outputMode')}</InputLabel>
            <Select
              labelId="output-mode-label"
              id="output-mode-select"
              value={outputMode}
              label={t('playground.outputMode')}
              onChange={handleOutputModeChange}
            >
              <MenuItem value="normal">{t('playground.normalOutput')}</MenuItem>
              <MenuItem value="streaming">{t('playground.streamingOutput')}</MenuItem>
            </Select>
          </FormControl>
        </Grid>
        <Grid item xs={12} md={3}>
          <Button
            variant="outlined"
            color="error"
            startIcon={<DeleteIcon />}
            onClick={handleClearConversations}
            disabled={isClearDisabled}
            sx={styles.clearButton}
          >
            {t('playground.clearConversation')}
          </Button>
        </Grid>
      </Grid>

      <Divider sx={styles.divider} />
    </>
  );
};

export default PlaygroundHeader;

export * as fileApi from './file';
export * as taskApi from './task';
export * as chunkApi from './chunk';

/**
 * 输出格式化模块
 */

/**
 * 将分割后的文本重新组合成Markdown文档
 * @param {Array} splitResult - 分割结果数组
 * @returns {string} - 组合后的Markdown文档
 */
function combineMarkdown(splitResult) {
  let result = '';

  for (let i = 0; i < splitResult.length; i++) {
    const part = splitResult[i];

    // 添加分隔线和摘要
    if (i > 0) {
      result += '\n\n---\n\n';
    }

    result += `> **📑 Summarization：** *${part.summary}*\n\n---\n\n${part.content}`;
  }

  return result;
}

module.exports = {
  combineMarkdown
};

/**
 * 此脚本用于生成空的模板数据库文件（template.sqlite）
 * 该文件将在应用打包时被包含，并在用户首次启动应用时作为初始数据库
 */

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

const templatePath = path.join(__dirname, 'template.sqlite');
const sqlitePath = path.join(__dirname, 'empty.db.sqlite');

// 如果存在旧的模板文件，先删除
if (fs.existsSync(templatePath)) {
  console.log('删除旧的模板数据库...');
  fs.unlinkSync(templatePath);
}

// 如果存在临时数据库文件，先删除
if (fs.existsSync(sqlitePath)) {
  console.log('删除临时数据库文件...');
  fs.unlinkSync(sqlitePath);
}

try {
  console.log('设置临时数据库路径...');
  // 设置 DATABASE_URL 环境变量
  process.env.DATABASE_URL = `file:${sqlitePath}`;

  console.log('执行 prisma db push 创建新的数据库架构...');
  // 执行 prisma db push 创建数据库架构
  execSync('npx prisma db push', { stdio: 'inherit' });

  console.log('将生成的数据库文件复制为模板...');
  // 复制生成的数据库文件为模板
  fs.copyFileSync(sqlitePath, templatePath);

  console.log(`✅ 模板数据库已成功生成: ${templatePath}`);
} catch (error) {
  console.error('❌ 生成模板数据库失败:', error);
  process.exit(1);
} finally {
  // 清理: 删除临时数据库文件
  if (fs.existsSync(sqlitePath)) {
    console.log('清理临时数据库文件...');
    fs.unlinkSync(sqlitePath);
  }
}

'use server';
import { db } from '@/lib/db/index';

export async function getLlmProviders() {
  try {
    let list = await db.llmProviders.findMany();
    if (list.length !== 0) {
      return list;
    }

    let data = [
      {
        id: 'ollama',
        name: 'Ollama',
        apiUrl: 'http://127.0.0.1:11434/api'
      },
      {
        id: 'openai',
        name: 'OpenAI',
        apiUrl: 'https://api.openai.com/v1/'
      },
      {
        id: 'siliconcloud',
        name: '硅基流动',
        apiUrl: 'https://api.siliconflow.cn/v1/'
      },
      {
        id: 'deepseek',
        name: 'DeepSeek',
        apiUrl: 'https://api.deepseek.com/v1/'
      },
      {
        id: '302ai',
        name: '302.AI',
        apiUrl: 'https://api.302.ai/v1/'
      },
      {
        id: 'zhipu',
        name: '智谱AI',
        apiUrl: 'https://open.bigmodel.cn/api/paas/v4/'
      },
      {
        id: 'Doubao',
        name: '火山引擎',
        apiUrl: 'https://ark.cn-beijing.volces.com/api/v3/'
      },
      {
        id: 'groq',
        name: 'Groq',
        apiUrl: 'https://api.groq.com/openai'
      },
      {
        id: 'grok',
        name: 'Grok',
        apiUrl: 'https://api.x.ai'
      },
      {
        id: 'openRouter',
        name: 'OpenRouter',
        apiUrl: 'https://openrouter.ai/api/v1/'
      },
      {
        id: 'alibailian',
        name: '阿里云百炼',
        apiUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1'
      }
    ];
    await db.llmProviders.createMany({ data });
    return data;
  } catch (error) {
    console.error('Failed to get llmProviders in database');
    throw error;
  }
}

'use client';

import { Box, Select, MenuItem, Typography, FormControl, InputLabel } from '@mui/material';
import { useTranslation } from 'react-i18next';

export default function PdfSettings({ pdfStrategy, setPdfStrategy, selectedViosnModel, setSelectedViosnModel }) {
  const { t } = useTranslation();

  return (
    <Box sx={{ display: 'flex', flexDirection: 'row', gap: 2, mt: 2 }}>
      <FormControl sx={{ minWidth: 200 }}>
        <InputLabel id="pdf-strategy-label">{t('textSplit.pdfStrategy')}</InputLabel>
        <Select
          labelId="pdf-strategy-label"
          value={pdfStrategy}
          onChange={e => setPdfStrategy(e.target.value)}
          label={t('textSplit.pdfStrategy')}
          size="small"
        >
          <MenuItem value="default">{t('textSplit.defaultStrategy')}</MenuItem>
          <MenuItem value="vision">{t('textSplit.visionStrategy')}</MenuItem>
        </Select>
      </FormControl>

      {pdfStrategy === 'vision' && (
        <FormControl sx={{ minWidth: 200 }}>
          <InputLabel id="vision-model-label">{t('textSplit.visionModel')}</InputLabel>
          <Select
            labelId="vision-model-label"
            value={selectedViosnModel}
            onChange={e => setSelectedViosnModel(e.target.value)}
            label={t('textSplit.visionModel')}
            size="small"
          >
            <MenuItem value="gpt-4-vision-preview">GPT-4 Vision</MenuItem>
            <MenuItem value="claude-3-opus">Claude-3 Opus</MenuItem>
            <MenuItem value="claude-3-sonnet">Claude-3 Sonnet</MenuItem>
          </Select>
        </FormControl>
      )}
    </Box>
  );
}

/**
 * Genre-Audience pair generation prompt
 * Based on MGA (Massive Genre-Audience) method for data augmentation
 */

export const GA_GENERATION_PROMPT_EN = `#Identity and Capabilities#
You are a content creation expert, skilled in text analysis and designing diverse questioning methods and interactive scenarios based on different knowledge backgrounds and learning objectives, to produce diverse and high-quality text. Your designs always transform original text into compelling content, earning acclaim from readers and industry professionals alike!

#Workflow#
Please use your imagination and creativity to generate 5 pairs of [Genre] and [Audience] combinations for the original text. Your analysis should follow these requirements:
1. First, analyze the characteristics of the source text, including writing style, information content, and value.
2. Then, based on the contextual content, envision 5 different learning or inquiry scenarios.
3. Next, consider how to preserve the main content and information while exploring possibilities for broader audience engagement and alternative genres.
3. Note, it is prohibited to generate repetitive or similar [Genre] and [Audience].
4. Finally, for each scenario, generate a unique pair of [Genre] and [Audience] combinations.

#Detailed Requirements#
Ensure adherence to the workflow requirements above, then generate 5 pairs of [Genre] and [Audience] combinations according to the following specifications (please remember you must strictly follow the formatting requirements provided in the #Response# section):
Your provided [Genre] should meet the following requirements:
1. Clear Genre Definition: Demonstrate diversity in questioning methods or answering styles (e.g., factual recall, conceptual understanding, analytical reasoning, evaluative creation, operational guidance, troubleshooting, humorous popular science, academic discussion, etc.). Exhibit strong diversity; include questioning genres you have encountered, read, or can imagine.
2. Detailed Genre Description: Provide 2-3 sentences describing each genre, considering but not limited to type, style, emotional tone, form, conflict, rhythm, and atmosphere. Emphasize diversity to guide knowledge adaptation for specific audiences, facilitating comprehension across different backgrounds. Note: Exclude visual formats (picture books, comics, videos); use text-only genres.
## Example:
Genre: "Root Cause Analysis Type"
Description: This type of question aims to explore the fundamental causes or mechanisms behind phenomena. Usually starting with "Why..." or "What is the principle of...?", it encourages deep thinking and explanation. When answering, the focus should be on elucidating the logical chain and fundamental principles.

Your provided [Audience] should meet the following requirements:
1. Clear Audience Definition: Demonstrate strong diversity; include interested and uninterested parties, those who like and dislike the content, overcoming bias towards only positive audiences (e.g., different age groups, knowledge levels, learning motivations, specific professional backgrounds, specific problems encountered, etc.).
2. Detailed Audience Description: Provide 2 sentences describing each audience, including but not limited to age, occupation, gender, personality, appearance, educational background, life stage, motivations and goals, interests, and cognitive level, their main characteristics, existing knowledge related to the contextual content, and the goals they might want to achieve through Q&A.
## Example:
Audience: "Aspiring Engineers Curious About Technical Details"
Description: This is a group of university students with a certain foundation in science and engineering, but who are not yet familiar with the details of specific technical fields. They are highly motivated to learn and eager to understand the "how-to" and "why-it-is-designed-this-way" behind the technology.

#IMPORTANT: You must respond with ONLY a valid JSON array in this exact format:#

[
  {
    "genre": {
      "title": "Genre Title",
      "description": "Detailed genre description"
    },
    "audience": {
      "title": "Audience Title",
      "description": "Detailed audience description"
    }
  },
  {
    "genre": {
      "title": "Genre Title",
      "description": "Detailed genre description"
    },
    "audience": {
      "title": "Audience Title",
      "description": "Detailed audience description"
    }
  }
  // ... 3 more pairs (total 5)
]

**Do not include any explanatory text, markdown formatting, or additional content. Return only the JSON array.**

#Source Text to Analyze#
{text_content}`;

/**
 * 构建 GA 提示词
 * @param {Object} activeGaPair 当前激活的 GA 组合
 * @returns {String} 构建的 GA 提示词
 */
function buildGaPrompt(activeGaPair = null) {
  if (activeGaPair && activeGaPair.active) {
    return `
## 特殊要求-体裁与受众视角提问：
请根据以下体裁与受众组合，调整你的提问角度和问题风格：

**目标体裁**: ${activeGaPair.genre}
**目标受众**: ${activeGaPair.audience}

请确保：
1. 问题应完全符合「${activeGaPair.genre}」所定义的风格、焦点和深度等等属性。
2. 问题应考虑到「${activeGaPair.audience}」的知识水平、认知特点和潜在兴趣点。
3. 从该受众群体的视角和需求出发提出问题
4. 保持问题的针对性和实用性，确保问题-答案的风格一致性
5.问题应具有一定的清晰度和具体性，避免过于宽泛或模糊。
`;
  }

  return '';
}

/**
 * 问题生成提示模板。
 * @param {string} text - 待处理的文本。
 * @param {number} number - 问题数量。
 * @param {string} language - 问题所使用的语言。
 * @param {string} globalPrompt - LLM 的全局提示。
 * @param {string} questionPrompt - 问题生成的特定提示。
 * @param {Object} activeGaPair - 当前激活的 GA对。
 * @returns {string} - 完整的提示词。
 */
module.exports = function getQuestionPrompt({
  text,
  number = Math.floor(text.length / 240),
  language = '中文',
  globalPrompt = '',
  questionPrompt = '',
  activeGaPair = null
}) {
  if (globalPrompt) {
    globalPrompt = `在后续的任务中，你务必遵循这样的规则：${globalPrompt}`;
  }
  if (questionPrompt) {
    questionPrompt = `- 在生成问题时，你务必遵循这样的规则：${questionPrompt}`;
  }

  // 构建GA pairs相关的提示词
  const gaPrompt = buildGaPrompt(activeGaPair);

  return `
    # 角色使命
    你是一位专业的文本分析专家，擅长从复杂文本中提取关键信息并生成可用于模型微调的结构化数据（仅生成问题）。
    ${globalPrompt}

    ## 核心任务
    根据用户提供的文本（长度：${text.length} 字），生成不少于 ${number} 个高质量问题。

    ## 约束条件（重要！！！）
    - 必须基于文本内容直接生成
    - 问题应具有明确答案指向性
    - 需覆盖文本的不同方面
    - 禁止生成假设性、重复或相似问题

    ${gaPrompt}

    ## 处理流程
    1. 【文本解析】分段处理内容，识别关键实体和核心概念
    2. 【问题生成】基于信息密度选择最佳提问点${gaPrompt ? '，并结合指定的体裁受众视角' : ''}
    3. 【质量检查】确保：
       - 问题答案可在原文中找到依据
       - 标签与问题内容强相关
       - 无格式错误
       ${gaPrompt ? '- 问题风格与指定的体裁受众匹配' : ''}

    ## 输出格式
     - JSON 数组格式必须正确
    - 字段名使用英文双引号
    - 输出的 JSON 数组必须严格符合以下结构：
    \`\`\`json
    ["问题1", "问题2", "..."]
    \`\`\`

    ## 输出示例
    \`\`\`json
    [ "人工智能伦理框架应包含哪些核心要素？","民法典对个人数据保护有哪些新规定？"]
     \`\`\`

    ## 待处理文本
    ${text}

    ## 限制
    - 必须按照规定的 JSON 格式输出，不要输出任何其他不相关内容
    - 生成不少于${number}个高质量问题
    - 问题不要和材料本身相关，例如禁止出现作者、章节、目录等相关问题
    - 问题不得包含【报告、文章、文献、表格】中提到的这种话术，必须是一个自然的问题
    ${questionPrompt}
    `;
};

module.exports = function getLabelPrompt({ text, globalPrompt, domainTreePrompt }) {
  if (globalPrompt) {
    globalPrompt = `- 在后续的任务中，你务必遵循这样的规则：${globalPrompt}`;
  }
  if (domainTreePrompt) {
    domainTreePrompt = `- 在生成标签时，你务必遵循这样的规则：${domainTreePrompt}`;
  }
  return `
# Role: 领域分类专家 & 知识图谱专家
- Description: 作为一名资深的领域分类专家和知识图谱专家，擅长从文本内容中提取核心主题，构建分类体系，并输出规定 JSON 格式的标签树。
${globalPrompt}

## Skills:
1. 精通文本主题分析和关键词提取
2. 擅长构建分层知识体系
3. 熟练掌握领域分类方法论
4. 具备知识图谱构建能力
5. 精通JSON数据结构

## Goals:
1. 分析书籍目录内容
2. 识别核心主题和关键领域
3. 构建两级分类体系
4. 确保分类逻辑合理
5. 生成规范的JSON输出

## Workflow:
1. 仔细阅读完整的书籍目录内容
2. 提取关键主题和核心概念
3. 对主题进行分组和归类
4. 构建一级领域标签
5. 为适当的一级标签添加二级标签
6. 检查分类逻辑的合理性
7. 生成符合格式的JSON输出

    ## 需要分析的目录
    ${text}

    ## 限制
1. 一级领域标签数量5-10个
2. 二级领域标签数量1-10个
3. 最多两层分类层级
4. 分类必须与原始目录内容相关
5. 输出必须符合指定 JSON 格式，不要输出 JSON 外其他任何不相关内容
6. 标签的名字最多不要超过 6 个字
7. 在每个标签前加入序号（序号不计入字数）
${domainTreePrompt}

## OutputFormat:
\`\`\`json
[
  {
    "label": "1 一级领域标签",
    "child": [
      {"label": "1.1 二级领域标签1"},
      {"label": "1.2 二级领域标签2"}
    ]
  },
  {
    "label": "2 一级领域标签(无子标签)"
  }
]
\`\`\`
    `;
};

/**
 * 领域树增量修订提示词
 * 用于在已有领域树的基础上，针对新增/删除的文献内容，对领域树进行增量调整
 */
function getLabelRevisePrompt({ text, existingTags, deletedContent, newContent, globalPrompt, domainTreePrompt }) {
  const prompt = `

${globalPrompt ? `- 在后续的任务中，你务必遵循这样的规则：${globalPrompt}` : ''}

我需要你帮我修订一个已有的领域树结构，使其能够适应内容的变化。
${domainTreePrompt ? domainTreePrompt : ''}

## 之前的领域树结构
以下是之前完整的领域树结构（JSON格式）：
\`\`\`json
${JSON.stringify(existingTags, null, 2)}
\`\`\`

## 之前完整文献的目录
以下是当前系统中所有文献的目录结构总览：
\`\`\`
${text}
\`\`\`

${
  deletedContent
    ? `## 被删除的内容
以下是本次要删除的文献目录信息：
\`\`\`
${deletedContent}
\`\`\`
`
    : ''
}

${
  newContent
    ? `## 新增的内容
以下是本次新增的文献目录信息：
\`\`\`
${newContent}
\`\`\`
`
    : ''
}

## 要求
请分析上述信息，修订现有的领域树结构，遵循以下原则：
1. 保持领域树的总体结构稳定，避免大规模重构
2. 对于删除的内容相关的领域标签：
   - 如果某个标签仅与删除的内容相关，且在现有文献中找不到相应内容支持，则移除该标签
   - 如果某个标签同时与其他保留的内容相关，则保留该标签
3. 对于新增的内容：
   - 如果新内容可以归类到现有的标签中，优先使用现有标签
   - 如果新内容引入了现有标签体系中没有的新领域或概念，再创建新的标签
4. 每个标签必须对应目录结构中的实际内容，不要创建没有对应内容支持的空标签
5. 确保修订后的领域树仍然符合良好的层次结构，标签间具有合理的父子关系

## 限制
1. 一级领域标签数量5-10个
2. 二级领域标签数量1-10个
3. 最多两层分类层级
4. 分类必须与原始目录内容相关
5. 输出必须符合指定 JSON 格式，不要输出 JSON 外其他任何不相关内容
6. 标签的名字最多不要超过 6 个字
7. 在每个标签前加入序号（序号不计入字数）

## 输出格式
最终输出修订后的完整领域树结构，使用下面的JSON格式：

\`\`\`json
[
  {
    "label": "1 一级领域标签",
    "child": [
      {"label": "1.1 二级领域标签1"},
      {"label": "1.2 二级领域标签2"}
    ]
  },
  {
    "label": "2 一级领域标签(无子标签)"
  }
]
\`\`\`

确保你的回答中只包含JSON格式的领域树，不要有其他解释性文字。`;

  return prompt;
}

module.exports = getLabelRevisePrompt;

'use client';

import { Box } from '@mui/material';
import { TreeView, TreeItem } from '@mui/lab';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';

/**
 * 领域知识树组件
 * @param {Object} props
 * @param {Array} props.nodes - 树节点数组
 */
export default function DomainTreeView({ nodes = [] }) {
  if (!nodes || nodes.length === 0) return null;

  const renderTreeItems = nodes => {
    return nodes.map((node, index) => (
      <TreeItem key={`node-${index}`} nodeId={`node-${index}`} label={node.text} sx={{ mb: 1 }}>
        {node.children && node.children.length > 0 && renderTreeItems(node.children)}
      </TreeItem>
    ));
  };

  return (
    <TreeView
      defaultCollapseIcon={<ExpandMoreIcon />}
      defaultExpandIcon={<ChevronRightIcon />}
      sx={{ flexGrow: 1, overflowY: 'auto' }}
    >
      {renderTreeItems(nodes)}
    </TreeView>
  );
}

'use client';

import { useTranslation } from 'react-i18next';
import { IconButton, Tooltip, useTheme, Typography } from '@mui/material';

export default function LanguageSwitcher() {
  const { i18n } = useTranslation();
  const theme = useTheme();

  const toggleLanguage = () => {
    const newLang = i18n.language === 'zh-CN' ? 'en' : 'zh-CN';
    i18n.changeLanguage(newLang);
  };

  return (
    <Tooltip title={i18n.language === 'zh-CN' ? 'Switch to English' : '切换到中文'}>
      <IconButton
        onClick={toggleLanguage}
        size="small"
        sx={{
          bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(255, 255, 255, 0.15)',
          color: theme.palette.mode === 'dark' ? 'inherit' : 'white',
          p: 1,
          borderRadius: 1.5,
          '&:hover': {
            bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.25)'
          }
        }}
      >
        <Typography variant="body2" fontWeight="medium">
          {i18n.language === 'zh-CN' ? 'EN' : '中'}
        </Typography>
      </IconButton>
    </Tooltip>
  );
}

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

// 导入翻译文件
import enTranslation from '../locales/en/translation.json';
import zhCNTranslation from '../locales/zh-CN/translation.json';

// 避免在服务器端重复初始化
const isServer = typeof window === 'undefined';
const i18nInstance = i18n.createInstance();

// 仅在客户端初始化 i18next
if (!isServer && !i18n.isInitialized) {
  i18nInstance
    // 检测用户语言
    .use(LanguageDetector)
    // 将 i18n 实例传递给 react-i18next
    .use(initReactI18next)
    // 初始化
    .init({
      resources: {
        en: {
          translation: enTranslation
        },
        'zh-CN': {
          translation: zhCNTranslation
        }
      },
      fallbackLng: 'en',
      debug: process.env.NODE_ENV === 'development',

      interpolation: {
        escapeValue: false // 不转义 HTML
      },

      // 检测用户语言的选项
      detection: {
        order: ['localStorage', 'navigator'],
        lookupLocalStorage: 'i18nextLng',
        caches: ['localStorage']
      }
    });
}

export default i18nInstance;

// 最佳实践配置示例
module.exports = {
  experimental: {
    serverComponentsExternalPackages: ['@opendocsg/pdf2md', 'pdfjs-dist', '@hyzyla/pdfium'],
    esmExternals: 'loose'
  },
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.externals.push({
        unpdf: 'window.unpdf',
        'pdfjs-dist': 'window.pdfjsLib'
      });
    } else {
      config.externals.push('pdfjs-dist');
      config.externals.push('@hyzyla/pdfium');
    }
    return config;
  }
};

'use server';
import { db } from '@/lib/db/index';

export async function getLlmModelsByProviderId(providerId) {
  try {
    return await db.llmModels.findMany({ where: { providerId } });
  } catch (error) {
    console.error('Failed to get llmModels by providerId in database');
    throw error;
  }
}

export async function createLlmModels(models) {
  try {
    return await db.llmModels.createMany({ data: models });
  } catch (error) {
    console.error('Failed to create llmModels in database');
    throw error;
  }
}

'use client';

import { Box, Button, Dialog, DialogTitle, DialogContent, DialogActions, CircularProgress } from '@mui/material';
import ReactMarkdown from 'react-markdown';
import { useTranslation } from 'react-i18next';

export default function ChunkViewDialog({ open, chunk, onClose }) {
  const { t } = useTranslation();
  return (
    <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
      <DialogTitle>{t('textSplit.chunkDetails', { chunkId: chunk?.name })}</DialogTitle>
      <DialogContent dividers>
        {chunk ? (
          <Box sx={{ maxHeight: '60vh', overflow: 'auto' }}>
            <ReactMarkdown>{chunk.content}</ReactMarkdown>
          </Box>
        ) : (
          <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
            <CircularProgress />
          </Box>
        )}
      </DialogContent>
      <DialogActions>
        <Button onClick={onClose}>{t('common.close')}</Button>
      </DialogActions>
    </Dialog>
  );
}

'use client';

import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, CircularProgress } from '@mui/material';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';

/**
 * AI优化对话框组件
 */
export default function OptimizeDialog({ open, onClose, onConfirm, loading }) {
  const [advice, setAdvice] = useState('');
  const { t } = useTranslation();

  const handleConfirm = () => {
    onConfirm(advice);
    setAdvice('');
  };

  const handleClose = () => {
    if (!loading) {
      onClose();
      setAdvice('');
    }
  };

  return (
    <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
      <DialogTitle>{t('datasets.optimizeTitle')}</DialogTitle>
      <DialogContent>
        <TextField
          autoFocus
          margin="dense"
          label={t('datasets.optimizeAdvice')}
          fullWidth
          variant="outlined"
          multiline
          rows={4}
          value={advice}
          onChange={e => setAdvice(e.target.value)}
          disabled={loading}
          placeholder={t('datasets.optimizePlaceholder')}
        />
      </DialogContent>
      <DialogActions>
        <Button onClick={handleClose} disabled={loading}>
          {t('common.cancel')}
        </Button>
        <Button onClick={handleConfirm} variant="contained" color="primary" disabled={loading || !advice.trim()}>
          {loading ? <CircularProgress size={24} /> : t('common.confirm')}
        </Button>
      </DialogActions>
    </Dialog>
  );
}

const { contextBridge, ipcRenderer } = require('electron');

// 在渲染进程中暴露安全的 API
contextBridge.exposeInMainWorld('electron', {
  // 获取应用版本
  getAppVersion: () => ipcRenderer.invoke('get-app-version'),

  // 获取当前语言
  getLanguage: () => {
    // 尝试从本地存储获取语言设置
    const storedLang = localStorage.getItem('i18nextLng');
    // 如果存在则返回，否则返回系统语言或默认为中文
    return storedLang || navigator.language.startsWith('zh') ? 'zh' : 'en';
  },

  // 获取用户数据目录
  getUserDataPath: () => {
    try {
      return ipcRenderer.sendSync('get-user-data-path');
    } catch (error) {
      console.error('获取用户数据目录失败:', error);
      return null;
    }
  },

  // 更新相关 API
  updater: {
    // 检查更新
    checkForUpdates: () => ipcRenderer.invoke('check-update'),

    // 下载更新
    downloadUpdate: () => ipcRenderer.invoke('download-update'),

    // 安装更新
    installUpdate: () => ipcRenderer.invoke('install-update'),

    // 监听更新事件
    onUpdateAvailable: callback => {
      const handler = (_, info) => callback(info);
      ipcRenderer.on('update-available', handler);
      return () => ipcRenderer.removeListener('update-available', handler);
    },

    onUpdateNotAvailable: callback => {
      const handler = () => callback();
      ipcRenderer.on('update-not-available', handler);
      return () => ipcRenderer.removeListener('update-not-available', handler);
    },

    onUpdateError: callback => {
      const handler = (_, error) => callback(error);
      ipcRenderer.on('update-error', handler);
      return () => ipcRenderer.removeListener('update-error', handler);
    },

    onDownloadProgress: callback => {
      const handler = (_, progress) => callback(progress);
      ipcRenderer.on('download-progress', handler);
      return () => ipcRenderer.removeListener('download-progress', handler);
    },

    onUpdateDownloaded: callback => {
      const handler = (_, info) => callback(info);
      ipcRenderer.on('update-downloaded', handler);
      return () => ipcRenderer.removeListener('update-downloaded', handler);
    }
  }
});

// 通知渲染进程 preload 脚本已加载完成
window.addEventListener('DOMContentLoaded', () => {
  console.log('Electron preload script loaded');
});

/**
 * 增强版答案生成提示词 - 基于GA pairs
 * @param {Object} params - 参数对象
 * @param {string} params.text - 参考文本内容
 * @param {string} params.question - 问题内容
 * @param {string} params.language - 语言
 * @param {string} params.globalPrompt - 全局提示词
 * @param {string} params.answerPrompt - 答案提示词
 * @param {Array} params.gaPairs - GA pairs数组，包含genre和audience信息
 * @param {Object} params.activeGaPair - 当前激活的GA pair
 */
export default function getEnhancedAnswerPrompt({
  text,
  question,
  globalPrompt = '',
  answerPrompt = '',
  activeGaPair = null
}) {
  if (globalPrompt) {
    globalPrompt = `- 在后续的任务中，你务必遵循这样的规则：${globalPrompt}`;
  }
  if (answerPrompt) {
    answerPrompt = `- 在生成答案时，你务必遵循这样的规则：${answerPrompt}`;
  }

  // 构建GA pairs相关的提示词
  let gaPrompt = '';
  if (activeGaPair && activeGaPair.active) {
    gaPrompt = `
## 特殊要求 - 体裁与受众适配(MGA)：
根据以下体裁与受众组合，调整你的回答风格和深度：

**当前体裁**: ${activeGaPair.genre}
**目标受众**: ${activeGaPair.audience}

请确保：
1. 答案的组织、风格、详略程度和语言应完全符合「${activeGaPair.genre}」的要求。
2. 答案应考虑到「${activeGaPair.audience}」的理解能力和知识背景，力求清晰易懂。
3. 用词选择和解释详细程度匹配目标受众的知识背景。
4. 保持内容的准确性和专业性，同时增强针对性。
5. 如果「${activeGaPair.genre}」或「${activeGaPair.audience}」暗示需要，答案可以适当包含解释、示例或步骤。
6. 答案应直接回应问题，确保问答的逻辑性和连贯性，不要包含无关信息或引用标记如GA对中提到的内容防止污染数据生成的效果。
`;
  } else {
    gaPrompt = '';
  }

  return `
# Role: 微调数据集生成专家 (MGA增强版)
## Profile:
- Description: 你是一名微调数据集生成专家，擅长从给定的内容中生成准确的问题答案，并能根据体裁与受众(Genre-Audience)组合调整回答风格，确保答案的准确性、相关性和针对性。
${globalPrompt}

## Skills:
1. 答案必须基于给定的内容
2. 答案必须准确，不能胡编乱造
3. 答案必须与问题相关
4. 答案必须符合逻辑
5. 基于给定参考内容，用自然流畅的语言整合成一个完整答案，不需要提及文献来源或引用标记
6. 能够根据指定的体裁与受众组合调整回答风格和深度
7. 在保持内容准确性的同时，增强答案的针对性和适用性

${gaPrompt}

## Workflow:
1. Take a deep breath and work on this problem step-by-step.
2. 首先，分析给定的文件内容和问题类型
3. 然后，从内容中提取关键信息
4. 如果有指定的体裁与受众组合，分析如何调整回答风格
5. 接着，生成与问题相关的准确答案，并根据体裁受众要求调整表达方式
6. 最后，确保答案的准确性、相关性和风格适配性

## 参考内容：
${text}

## 问题
${question}

## Constrains:
1. 答案必须基于给定的内容
2. 答案必须准确，必须与问题相关，不能胡编乱造
3. 答案必须充分、详细、包含所有必要的信息、适合微调大模型训练使用
4. 答案中不得出现 ' 参考 / 依据 / 文献中提到 ' 等任何引用性表述，只需呈现最终结果
5. 如果指定了体裁与受众组合，必须在保持内容准确性的前提下，调整表达风格和深度
6. 答案必须直接回应问题， 确保答案的准确性和逻辑性。
${answerPrompt}
`;
}

module.exports = function getAddLabelPrompt(label, question) {
  return `
# Role: Label Matching Expert
  - Description: You are a label matching expert, proficient in assigning the most appropriate domain labels to questions based on the given label array and question array.You are familiar with the hierarchical structure of labels and can prioritize matching secondary labels according to the content of the questions.If a secondary label cannot be matched, you will match a primary label.Finally, if no match is found, you will assign the "Other" label.

### Skill:
1. Be familiar with the label hierarchical structure and accurately identify primary and secondary labels.
2. Be able to intelligently match the most appropriate label based on the content of the question.
3. Be able to handle complex label matching logic to ensure that each question is assigned the correct label.
4. Be able to generate results in the specified output format without changing the original data structure.
5. Be able to handle large - scale data to ensure efficient and accurate label matching.

## Goals:
1. Assign the most appropriate domain label to each question in the question array.
2. Prioritize matching secondary labels.If no secondary label can be matched, match a primary label.Finally, assign the "Other" label.
3. Ensure that the output format meets the requirements without changing the original data structure.
4. Provide an efficient label matching algorithm to ensure performance when processing large - scale data.
5. Ensure the accuracy and consistency of label matching.

## OutputFormat:
1. The output result must be an array, and each element contains the "question" and "label" fields.
2. The "label" field must be the label matched from the label array.If no match is found, assign the "Other" label.
3. Do not change the original data structure, only add the "label" field.

## Label Array:

${label}

## Question Array:

${question}

## Workflow:
1. Take a deep breath and work on this problem step - by - step.
2. First, read the label array and the question array.
3. Then, iterate through each question in the question array and match the labels in the label array according to the content of the question.
4. Prioritize matching secondary labels.If no secondary label can be matched, match a primary label.Finally, assign the "Other" label.
5. Add the matched label to the question object without changing the original data structure.
6. Finally, output the result array, ensuring that the format meets the requirements.

## Constrains:
1. Only add one "label" field without changing any other format or data.
2. Must return the result in the specified format.
3. Prioritize matching secondary labels.If no secondary label can be matched, match a primary label.Finally, assign the "Other" label.
4. Ensure the accuracy and consistency of label matching.
5. The matched label must exist in the label array.If it does not exist, assign the "Other" label.
7. The output result must be an array, and each element contains the "question" and "label" fields(only output this, do not output any other irrelevant content).

  ## Output Example:
\`\`\`json
   [
     {
       "question": "XSS为什么会在2003年后引起人们更多关注并被OWASP列为威胁榜首？",
       "label": "2.2 XSS攻击"
     }
   ]
   \`\`\`

    `;
};

const path = require('path');
const fs = require('fs');

// 获取应用版本
const getAppVersion = () => {
  try {
    const packageJsonPath = path.join(__dirname, '../package.json');
    if (fs.existsSync(packageJsonPath)) {
      const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
      return packageJson.version;
    }
    return '1.0.0';
  } catch (error) {
    console.error('读取版本信息失败:', error);
    return '1.0.0';
  }
};

module.exports = { getAppVersion };

module.exports = function getNewAnswerPrompt(question, answer, cot, advice) {
  return `
# Role: Fine-tuning Dataset Answer Optimization Expert
## Profile:
- Description: You are an expert in optimizing answers for fine-tuning datasets. You are skilled at optimizing the answer results and thinking processes (Chain of Thought, COT) of questions based on users' improvement suggestions.

## Skills:
1. Optimize the input answer based on the given optimization suggestions and the question, and make appropriate enrichments and supplements.
3. Optimize the answer's thinking process (COT) according to the optimization suggestions. Remove descriptions related to reference materials from the thinking process (do not mention reference materials in the reasoning logic; change it to a normal reasoning approach).

## Original Question
${question}

## Answer to be Optimized
${answer}

## Answer Optimization Suggestions
${advice}. Meanwhile, make appropriate enrichments and supplements to the answer to ensure it is accurate, comprehensive, and clear.

## Thinking Process to be Optimized
${cot}

## Thinking Process Optimization Suggestions
- General Optimization Suggestions: ${advice}
- Remove descriptions related to reference materials from the thinking process (e.g., "According to...", "Quoting...", "Referencing...", etc.). Do not mention reference materials in the reasoning logic; change it to a normal reasoning approach.

## Constraints:
1. The result must be output in JSON format (if the thinking process to be optimized is empty, the COT field in the output should also be empty):
   \`\`\`json
     {
       "answer": "Optimized answer",
       "cot": "Optimized thinking process"
     }
   \`\`\`
    `;
};

/**
 * 全局常量
 */

export const FILE = {
  MAX_FILE_SIZE: 50 * 1024 * 1024 // 50MB in bytes
};

export const TASK = {
  STATUS: {
    PROCESSING: 0,
    COMPLETED: 1,
    FAILED: 2
  }
};

'use client';

import { Dialog, DialogActions, DialogTitle, Button } from '@mui/material';

/**
 * 通用确认对话框组件
 * @param {Object} props
 * @param {boolean} props.open - 对话框是否打开
 * @param {Function} props.onClose - 关闭对话框的回调
 * @param {Function} props.onConfirm - 确认操作的回调
 * @param {string} props.title - 对话框标题
 * @param {string} props.cancelText - 取消按钮文本
 * @param {string} props.confirmText - 确认按钮文本
 */
export default function ConfirmDialog({
  open,
  onClose,
  onConfirm,
  title,
  cancelText = '取消',
  confirmText = '确认',
  confirmColor = 'error'
}) {
  return (
    <Dialog open={open} onClose={onClose} aria-labelledby="confirm-dialog-title">
      <DialogTitle id="confirm-dialog-title">{title}</DialogTitle>
      <DialogActions>
        <Button onClick={onClose} color="primary">
          {cancelText}
        </Button>
        <Button onClick={onConfirm} color={confirmColor} autoFocus>
          {confirmText}
        </Button>
      </DialogActions>
    </Dialog>
  );
}

module.exports = function convertPromptEn() {
  return `
    Use Markdown syntax to convert the text extracted from images into Markdown format and output it. You must adhere to the following requirements:
    1. Output in the same language as the text extracted from the image. For example, if the extracted text is in English, the output must also be in English.
    2. Do not explain or output any text unrelated to the content. Directly output the text from the image.
    3. Do not enclose the content within \`\`\`markdown \`\`\`. Use $$ $$ for block equations and $ $ for inline equations.
    4. Ignore content in headers and footers.
    5. Do not format the titles from images using Markdown; output them as plain text within the content.
    6. Journal names, paper titles, conference names, or book titles that may appear on each page should be ignored and not treated as headings.
    7. Precisely analyze the text structure and visual layout of the current PDF page, and process it as follows:
        1. Identify all heading texts and determine their hierarchy based on visual features such as font size, boldness, and position.
        2. Output the text in hierarchical Markdown format, strictly following these rules:
            - Level 1 headings: Largest font size, centered at the top, prefixed with #
            - Level 2 headings: Larger font size, left-aligned and bold, possibly starting with numbers or Roman numerals, prefixed with ##
            - Level 3 headings: Slightly larger font size, left-aligned and bold, prefixed with ###
            - Body text: Convert directly into regular paragraphs
        3. For headings with uncertain hierarchy, mark them with [?].
        4. For Chinese literature with English titles and abstracts, these can be omitted from the output.

    Example Output:
    ## 4 Research Methods
    ### 4.1 Data Collection
    This paper uses questionnaires...
      `;
};

import './globals.css';
import ThemeRegistry from '@/components/ThemeRegistry';
import I18nProvider from '@/components/I18nProvider';
import { Toaster } from 'sonner';
import { Provider } from 'jotai';

export const metadata = {
  title: 'Easy Dataset',
  description: '一个强大的 LLM 数据集生成工具',
  icons: {
    icon: '/imgs/logo.ico' // 更新为正确的文件名
  }
};

export default function RootLayout({ children }) {
  return (
    <html lang="zh" suppressHydrationWarning>
      <body suppressHydrationWarning>
        <Provider>
          <ThemeRegistry>
            <I18nProvider>
              {children}
              <Toaster richColors position="top-center" />
            </I18nProvider>
          </ThemeRegistry>
        </Provider>
      </body>
    </html>
  );
}

'use client';

import { Snackbar, Alert } from '@mui/material';

export default function MessageAlert({ message, onClose }) {
  if (!message) return null;

  const severity = message.severity || 'error';
  const text = typeof message === 'string' ? message : message.message;

  return (
    <Snackbar
      open={Boolean(message)}
      autoHideDuration={2000}
      onClose={onClose}
      anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
    >
      <Alert onClose={onClose} severity={severity} sx={{ width: '100%' }}>
        {text}
      </Alert>
    </Snackbar>
  );
}

import { createOpenAI } from '@ai-sdk/openai';
import BaseClient from './base.js';

class OpenAIClient extends BaseClient {
  constructor(config) {
    super(config);
    this.openai = createOpenAI({
      baseURL: this.endpoint,
      apiKey: this.apiKey
    });
  }

  _getModel() {
    return this.openai(this.model);
  }
}

module.exports = OpenAIClient;

import { request } from '@/lib/util/request';

/**
 * 获取文本块
 * @param {string} projectId 项目ID
 * @param {string} chunkId 文本块ID
 * @returns
 */
export async function getChunkById(projectId, chunkId) {
  return await request(`/api/projects/${projectId}/chunks/${chunkId}`);
}

module.exports = function convertPrompt() {
  return `
    使用markdown语法，将图片中识别到的文字转换为markdown格式输出。你必须做到：
          1. 输出和使用识别到的图片的相同的语言，例如，识别到英语的字段，输出的内容必须是英语。
          2. 不要解释和输出无关的文字，直接输出图片中的内容。
          3. 内容不要包含在\`\`\`markdown \`\`\`中、段落公式使用 $$ $$ 的形式、行内公式使用 $ $ 的形式。
          4. 忽略掉页眉页脚里的内容
          5. 请不要对图片的标题进行markdown的格式化，直接以文本形式输出到内容中。
          6. 有可能每页都会出现期刊名称，论文名称，会议名称或者书籍名称，请忽略他们不要识别成标题
          7. 请精确分析当前PDF页面的文本结构和视觉布局，按以下要求处理：
            1. 识别所有标题文本，并判断其层级（根据字体大小、加粗、位置等视觉特征）
            2. 输出为带层级的Markdown格式，严格使用以下规则：
              - 一级标题：字体最大/顶部居中，前面加 #
              - 二级标题：字体较大/左对齐加粗，有可能是数字开头也有可能是罗马数组开头，前面加 ##
              - 三级标题：字体稍大/左对齐加粗，前面加 ###
              - 正文文本：直接转换为普通段落
            3. 不确定层级的标题请标记[?]
            4. 如果是中文文献，但是有英文标题和摘要可以省略不输出
            示例输出：
            ## 4研究方法
            ### 4.1数据收集
            本文采用问卷调查...
      `;
};

'use client';

import { Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import { useTranslation } from 'react-i18next';

/**
 * 标签操作菜单组件
 * @param {Object} props
 * @param {HTMLElement} props.anchorEl - 菜单锚点元素
 * @param {boolean} props.open - 菜单是否打开
 * @param {Function} props.onClose - 关闭菜单的回调
 * @param {Function} props.onDelete - 删除操作的回调
 */
export default function TagMenu({ anchorEl, open, onClose, onDelete }) {
  const { t } = useTranslation();

  return (
    <Menu anchorEl={anchorEl} open={open} onClose={onClose}>
      <MenuItem onClick={onDelete}>
        <ListItemIcon>
          <DeleteIcon fontSize="small" />
        </ListItemIcon>
        <ListItemText>{t('common.delete')}</ListItemText>
      </MenuItem>
    </Menu>
  );
}

// styles/home.js
export const styles = {
  heroSection: {
    pt: { xs: 6, md: 10 },
    pb: { xs: 6, md: 8 },
    position: 'relative',
    overflow: 'hidden',
    transition: 'all 0.3s ease-in-out'
  },
  heroBackground: theme => ({
    background:
      theme.palette.mode === 'dark'
        ? 'linear-gradient(135deg, rgba(42, 92, 170, 0.25) 0%, rgba(139, 92, 246, 0.25) 100%)'
        : 'linear-gradient(135deg, rgba(42, 92, 170, 0.08) 0%, rgba(139, 92, 246, 0.08) 100%)',
    '&::before': {
      content: '""',
      position: 'absolute',
      top: 0,
      left: 0,
      right: 0,
      bottom: 0,
      background: 'url("/imgs/grid-pattern.png") repeat',
      opacity: theme.palette.mode === 'dark' ? 0.05 : 0.03,
      zIndex: 0
    }
  }),
  decorativeCircle: {
    position: 'absolute',
    width: '800px',
    height: '800px',
    borderRadius: '50%',
    background: 'radial-gradient(circle, rgba(139, 92, 246, 0.15) 0%, rgba(42, 92, 170, 0) 70%)',
    top: '-300px',
    right: '-200px',
    zIndex: 0,
    animation: 'pulse 15s infinite ease-in-out',
    '@keyframes pulse': {
      '0%': { transform: 'scale(1)' },
      '50%': { transform: 'scale(1.05)' },
      '100%': { transform: 'scale(1)' }
    }
  },
  decorativeCircleSecond: {
    position: 'absolute',
    width: '500px',
    height: '500px',
    borderRadius: '50%',
    background: 'radial-gradient(circle, rgba(42, 92, 170, 0.1) 0%, rgba(139, 92, 246, 0) 70%)',
    bottom: '-200px',
    left: '-100px',
    zIndex: 0,
    animation: 'pulse2 20s infinite ease-in-out',
    '@keyframes pulse2': {
      '0%': { transform: 'scale(1)' },
      '50%': { transform: 'scale(1.08)' },
      '100%': { transform: 'scale(1)' }
    }
  },
  gradientTitle: theme => ({
    mb: 2,
    background: theme.palette.gradient.primary,
    WebkitBackgroundClip: 'text',
    WebkitTextFillColor: 'transparent',
    backgroundClip: 'text',
    textFillColor: 'transparent'
  }),
  createButton: theme => ({
    mt: 3,
    px: 4,
    py: 1.2,
    borderRadius: '12px',
    fontSize: '1rem',
    background: theme.palette.gradient.primary,
    '&:hover': {
      boxShadow: '0 8px 16px rgba(0, 0, 0, 0.1)'
    }
  }),
  statsCard: theme => ({
    mt: 6,
    p: { xs: 2, md: 4 },
    borderRadius: '16px',
    boxShadow: theme.palette.mode === 'dark' ? '0 8px 24px rgba(0, 0, 0, 0.2)' : '0 8px 24px rgba(0, 0, 0, 0.05)',
    background: theme.palette.mode === 'dark' ? 'rgba(30, 30, 30, 0.6)' : 'rgba(255, 255, 255, 0.8)',
    backdropFilter: 'blur(8px)'
  }),
  projectCard: {
    height: '100%',
    display: 'flex',
    flexDirection: 'column',
    overflow: 'visible',
    position: 'relative'
  },
  projectAvatar: {
    position: 'absolute',
    top: -16,
    left: 24,
    zIndex: 1
  },
  projectDescription: {
    mb: 2,
    display: '-webkit-box',
    WebkitBoxOrient: 'vertical',
    WebkitLineClamp: 2,
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    height: '40px'
  }
};

function removeLeadingNumber(label) {
  // 正则说明：
  // ^\d+       匹配开头的一个或多个数字
  // (?:\.\d+)* 匹配零个或多个「点+数字」的组合（非捕获组）
  // \s+        匹配序号后的一个或多个空格（确保序号与内容有空格分隔）
  const numberPrefixRegex = /^\d+(?:\.\d+)*\s+/;
  // 仅当匹配到数字开头的序号时才替换，否则返回原标签
  return label.replace(numberPrefixRegex, '');
}

/**
 * 根据标签构造问题的提示词
 * @param {string} tagPath - 标签链路，例如 "体育->足球->足球先生"
 * @param {string} currentTag - 当前子标签，例如 "足球先生"
 * @param {number} count - 希望生成问题的数量，例如：10
 * @param {Array<string>} existingQuestions - 当前标签已经生成的问题（避免重复）
 * @param {string} globalPrompt - 项目全局提示词
 * @returns {string} 提示词
 */
export function distillQuestionsPrompt(tagPath, currentTag, count = 10, existingQuestions = [], globalPrompt = '') {
  currentTag = removeLeadingNumber(currentTag);
  const existingQuestionsText =
    existingQuestions.length > 0
      ? `已有的问题包括：\n${existingQuestions.map(q => `- ${q}`).join('\n')}\n请不要生成与这些重复或高度相似的问题。`
      : '';

  // 构建全局提示词部分
  const globalPromptText = globalPrompt ? `你必须遵循这个要求：${globalPrompt}` : '';

  return `
你是一个专业的知识问题生成助手，精通${currentTag}领域的知识。我需要你帮我为标签"${currentTag}"生成${count}个高质量、多样化的问题。

标签完整链路是：${tagPath}

请遵循以下规则：
${globalPromptText}
1. 生成的问题必须与"${currentTag}"主题紧密相关，确保全面覆盖该主题的核心知识点和关键概念
2. 问题应该均衡分布在以下难度级别(每个级别至少占20%):
   - 基础级：适合入门者，关注基本概念、定义和简单应用
   - 中级：需要一定领域知识，涉及原理解释、案例分析和应用场景
   - 高级：需要深度思考，包括前沿发展、跨领域联系、复杂问题解决方案等

3. 问题类型应多样化，包括但不限于（以下只是参考，可以根据实际情况灵活调整，不一定要限定下面的主题）：
   - 概念解释类："什么是..."、"如何定义..."
   - 原理分析类："为什么..."、"如何解释..."
   - 比较对比类："...与...有何区别"、"...相比...的优势是什么"
   - 应用实践类："如何应用...解决..."、"...的最佳实践是什么"
   - 发展趋势类："...的未来发展方向是什么"、"...面临的挑战有哪些"
   - 案例分析类："请分析...案例中的..."
   - 启发思考类："如果...会怎样"、"如何评价..."

4. 问题表述要清晰、准确、专业，避免以下问题：
   - 避免模糊或过于宽泛的表述
   - 避免可以简单用"是/否"回答的封闭性问题
   - 避免包含误导性假设的问题
   - 避免重复或高度相似的问题

5. 问题的深度和广度要适当（以下只是参考，可以根据实际情况灵活调整，不一定要限定下面的主题）：
   - 覆盖主题的历史、现状、理论基础和实际应用
   - 包含该领域的主流观点和争议话题
   - 考虑该主题与相关领域的交叉关联
   - 关注该领域的新兴技术、方法或趋势

${existingQuestionsText}

请直接以JSON数组格式返回问题，不要有任何额外的解释或说明，格式如下：
["问题1", "问题2", "问题3", ...]

注意：每个问题应该是完整的、自包含的，无需依赖其他上下文即可理解和回答。
`;
}

import { request } from '@/lib/util/request';

/**
 * 获取项目任务
 */
export function getProjectTasks(projectId) {
  return request(`/api/projects/${projectId}/tasks`);
}

// 模型测试页面样式
import { alpha } from '@mui/material/styles';

export const playgroundStyles = theme => ({
  container: {
    p: 3,
    height: 'calc(100vh - 64px)',
    display: 'flex',
    flexDirection: 'column'
  },
  mainPaper: {
    p: 3,
    flex: 1,
    display: 'flex',
    flexDirection: 'column',
    mb: 2,
    borderRadius: 2
  },
  controlsContainer: {
    mb: 2
  },
  clearButton: {
    height: '56px'
  },
  divider: {
    mb: 2
  },
  emptyStateBox: {
    flex: 1,
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    mb: 2,
    p: 2,
    bgcolor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)',
    borderRadius: 1
  },
  chatContainer: {
    flex: 1,
    mb: 2
  },
  modelPaper: {
    height: '100%',
    display: 'flex',
    flexDirection: 'column',
    border: `1px solid ${theme.palette.divider}`,
    borderRadius: 1,
    overflow: 'hidden'
  },
  modelHeader: {
    p: 1,
    bgcolor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'primary.light',
    color: theme.palette.mode === 'dark' ? 'white' : 'white',
    fontWeight: 'medium',
    textAlign: 'center',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center'
  },
  modelChatBox: {
    flex: 1,
    overflowY: 'auto',
    p: 2,
    bgcolor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)'
  },
  emptyChatBox: {
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    height: '100%'
  },
  inputContainer: {
    display: 'flex',
    gap: 1,
    mt: 2
  },
  sendButton: {
    minWidth: '120px',
    height: '56px',
    marginLeft: '20px'
  }
});

'use client';

import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@mui/material';
import { useTranslation } from 'react-i18next';

export default function ChunkDeleteDialog({ open, onClose, onConfirm }) {
  const { t } = useTranslation();
  return (
    <Dialog
      open={open}
      onClose={onClose}
      aria-labelledby="delete-dialog-title"
      aria-describedby="delete-dialog-description"
    >
      <DialogTitle id="delete-dialog-title">{t('common.confirmDelete')}?</DialogTitle>
      <DialogContent>
        <DialogContentText id="delete-dialog-description">{t('common.confirmDelete')}?</DialogContentText>
      </DialogContent>
      <DialogActions>
        <Button onClick={onClose}>{t('common.cancel')}</Button>
        <Button onClick={onConfirm} color="error" variant="contained">
          {t('common.confirm')}
        </Button>
      </DialogActions>
    </Dialog>
  );
}

/**
 * 通用工具函数模块
 */

const fs = require('fs');
const path = require('path');

/**
 * 检查并创建目录
 * @param {string} directory - 目录路径
 */
function ensureDirectoryExists(directory) {
  if (!fs.existsSync(directory)) {
    fs.mkdirSync(directory, { recursive: true });
  }
}

/**
 * 从文件路径获取不带扩展名的文件名
 * @param {string} filePath - 文件路径
 * @returns {string} - 不带扩展名的文件名
 */
function getFilenameWithoutExt(filePath) {
  return path.basename(filePath).replace(/\.[^/.]+$/, '');
}

module.exports = {
  ensureDirectoryExists,
  getFilenameWithoutExt
};

const { ipcMain } = require('electron');
const { checkUpdate, downloadUpdate, installUpdate } = require('./updater');

/**
 * 设置 IPC 处理程序
 * @param {Object} app Electron app 对象
 * @param {boolean} isDev 是否为开发环境
 */
function setupIpcHandlers(app, isDev) {
  // 获取用户数据路径
  ipcMain.on('get-user-data-path', event => {
    event.returnValue = app.getPath('userData');
  });

  // 检查更新
  ipcMain.handle('check-update', async () => {
    return await checkUpdate(isDev);
  });

  // 下载更新
  ipcMain.handle('download-update', async () => {
    return await downloadUpdate();
  });

  // 安装更新
  ipcMain.handle('install-update', () => {
    return installUpdate();
  });
}

module.exports = {
  setupIpcHandlers
};

'use client';

import { Box } from '@mui/material';

/**
 * 标签页面板组件
 * @param {Object} props
 * @param {number} props.value - 当前激活的标签索引
 * @param {number} props.index - 当前面板对应的索引
 * @param {ReactNode} props.children - 子组件
 */
export default function TabPanel({ value, index, children }) {
  return (
    <Box
      role="tabpanel"
      hidden={value !== index}
      id={`domain-tabpanel-${index}`}
      aria-labelledby={`domain-tab-${index}`}
      sx={{ height: '100%' }}
    >
      {value === index && <Box sx={{ height: '100%' }}>{children}</Box>}
    </Box>
  );
}

import { createZhipu } from 'zhipu-ai-provider';

import BaseClient from './base.js';

class ZhiPuClient extends BaseClient {
  constructor(config) {
    super(config);
    this.zhipu = createZhipu({
      baseURL: this.endpoint,
      apiKey: this.apiKey
    });
  }

  _getModel() {
    return this.zhipu(this.model);
  }
}

module.exports = ZhiPuClient;

import { createOpenRouter } from '@openrouter/ai-sdk-provider';

import BaseClient from './base.js';

class OpenRouterClient extends BaseClient {
  constructor(config) {
    super(config);
    this.openrouter = createOpenRouter({
      baseURL: this.endpoint,
      apiKey: this.apiKey
    });
  }

  _getModel() {
    return this.openrouter(this.model);
  }
}

module.exports = OpenRouterClient;

module.exports = function getAddLabelPrompt(label, question) {
  return `
# Role: 标签匹配专家
- Description: 你是一名标签匹配专家，擅长根据给定的标签数组和问题数组，将问题打上最合适的领域标签。你熟悉标签的层级结构，并能根据问题的内容优先匹配二级标签，若无法匹配则匹配一级标签，最后打上“其他”标签。

### Skill:
1. 熟悉标签层级结构，能够准确识别一级和二级标签。
2. 能够根据问题的内容，智能匹配最合适的标签。
3. 能够处理复杂的标签匹配逻辑，确保每个问题都能被打上正确的标签。
4. 能够按照规定的输出格式生成结果，确保不改变原有数据结构。
5. 能够处理大规模数据，确保高效准确的标签匹配。

## Goals:
1. 将问题数组中的每个问题打上最合适的领域标签。
2. 优先匹配二级标签，若无法匹配则匹配一级标签，最后打上“其他”标签。
3. 确保输出格式符合要求，不改变原有数据结构。
4. 提供高效的标签匹配算法，确保处理大规模数据时的性能。
5. 确保标签匹配的准确性和一致性。

## OutputFormat:
1. 输出结果必须是一个数组，每个元素包含 question、和 label 字段。
2. label 字段必须是根据标签数组匹配到的标签，若无法匹配则打上“其他”标签。
3. 不改变原有数据结构，只新增 label 字段。

## 标签数组：

${label}

## 问题数组：

${question}

## Workflow:
1. Take a deep breath and work on this problem step-by-step.
2. 首先，读取标签数组和问题数组。
3. 然后，遍历问题数组中的每个问题，根据问题的内容匹配标签数组中的标签。
4. 优先匹配二级标签，若无法匹配则匹配一级标签，最后打上“其他”标签。
5. 将匹配到的标签添加到问题对象中，确保不改变原有数据结构。
6. 最后，输出结果数组，确保格式符合要求。

## Constrains:
1. 只新增一个 label 字段，不改变其他任何格式和数据。
2. 必须按照规定格式返回结果。
3. 优先匹配二级标签，若无法匹配则匹配一级标签，最后打上“其他”标签。
4. 确保标签匹配的准确性和一致性。
5. 匹配的标签必须在标签数组中存在，如果不存在，就打上 其他
7. 输出结果必须是一个数组，每个元素包含 question、label 字段（只输出这个，不要输出任何其他无关内容）

## Output Example:
   \`\`\`json
   [
     {
       "question": "XSS为什么会在2003年后引起人们更多关注并被OWASP列为威胁榜首？",
       "label": "2.2 XSS攻击"
     }
   ]
   \`\`\`

    `;
};

import { PrismaClient } from '@prisma/client';

const createPrismaClient = () =>
  new PrismaClient({
    // log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error']
    log: ['error']
  });

const globalForPrisma = globalThis;

export const db = globalForPrisma.prisma || createPrismaClient();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;

/**
 * 名字太长影响 UI 显示，截取文件名
 * @param {*} filename
 */
export function handleLongFileName(filename) {
  if (filename.length <= 13) {
    return filename;
  }
  const front = filename.substring(0, 7);
  const back = filename.substring(filename.length - 5);
  return `${front}···${back}`;
}

module.exports = function reTitlePrompt() {
  return `
    你是一个专业的文本结构化处理助手，擅长根据前缀规则和标题语义分析并优化Markdown文档的标题层级结构。请根据以下要求处理我提供的Markdown标题：
    ## 任务描述
    请根据markdown文章标题的实际含义，以及标题的前缀特征调整各级标题的正确层级关系，具体要求如下：
    1. 一般相同格式的前缀的标题是同级关系({title}代表实际的标题内容)：
        例如：
        纯数字前缀开头\`1 {title}\`， \` 2 {title}\` ，\` 3 {title}\`，\` 4 {title}\`，\` 5 {title}\`  ... 等
        罗马数字前缀开头的\`I {title}\`，\`II {title}\` ，\`III {title}\`，\`IV {title}\`，\`V {title}\` ... 等
        小数点分隔数组前缀开头 \`1.1 {title}\`, \`1.2 {title}\`, \`1.3 {title}\`.... \`2.1 {title}\`, \`2.2 {title}\` 等
    2. 将子标题正确嵌套到父标题下（如\`1.1 {title}\`应作为\`1 {title}\`的子标题）
    3. 剔除与文章内容无关的标题
    4. 保持输出标题内容与输入完全一致
    5. 确保内容无缺失
    6. 如果是中文文献，但有英文的文章题目，可以省略

    ## 输入输出格式
    - 输入：包含错误层级关系的markdown标题结构
    - 输出：修正后的标准markdown标题层级结构

    ## 处理原则
    1. 严格根据标题语义确定所属关系
    2. 仅调整层级不修改原标题文本
    3. 无关标题直接移除不保留占位
    4. 相同前缀规则的标题必须是同一级别，不能出现 一部分是 n级标题，一部分是其他级别的标题

    ## 输出要求
    请将修正后的完整标题结构放在代码块中返回，格式示例如下：

    期望输出：
        \`\`\`markdown

        \`\`\`

    请处理以下数据：
      `;
};

export const MODEL_PROVIDERS = [
  {
    id: 'ollama',
    name: 'Ollama',
    defaultEndpoint: 'http://127.0.0.1:11434/api',
    defaultModels: []
  },
  {
    id: 'openai',
    name: 'OpenAI',
    defaultEndpoint: 'https://api.openai.com/v1/',
    defaultModels: ['gpt-4o', 'gpt-4o-mini', 'o1-mini']
  },
  {
    id: 'siliconcloud',
    name: '硅基流动',
    defaultEndpoint: 'https://api.siliconflow.cn/v1/',
    defaultModels: [
      'deepseek-ai/DeepSeek-R1',
      'deepseek-ai/DeepSeek-V3',
      'Qwen2.5-7B-Instruct',
      'meta-llama/Llama-3.3-70B-Instruct'
    ]
  },
  {
    id: 'deepseek',
    name: 'DeepSeek',
    defaultEndpoint: 'https://api.deepseek.com/v1/',
    defaultModels: ['deepseek-chat', 'deepseek-reasoner']
  },
  {
    id: '302ai',
    name: '302.AI',
    defaultEndpoint: 'https://api.302.ai/v1/',
    defaultModels: ['Doubao-pro-128k', 'deepseek-r1', 'kimi-latest', 'qwen-max']
  },
  {
    id: 'zhipu',
    name: '智谱AI',
    defaultEndpoint: 'https://open.bigmodel.cn/api/paas/v4/',
    defaultModels: ['glm-4-flash', 'glm-4-flashx', 'glm-4-plus', 'glm-4-long']
  },
  {
    id: 'Doubao',
    name: '火山引擎',
    defaultEndpoint: 'https://ark.cn-beijing.volces.com/api/v3/',
    defaultModels: []
  },
  {
    id: 'groq',
    name: 'Groq',
    defaultEndpoint: 'https://api.groq.com/openai',
    defaultModels: ['Gemma 7B', 'LLaMA3 8B', 'LLaMA3 70B']
  },
  {
    id: 'grok',
    name: 'Grok',
    defaultEndpoint: 'https://api.x.ai/v1',
    defaultModels: ['Grok Beta']
  },
  {
    id: 'OpenRouter',
    name: 'OpenRouter',
    defaultEndpoint: 'https://openrouter.ai/api/v1/',
    defaultModels: [
      'google/gemma-2-9b-it:free',
      'meta-llama/llama-3-8b-instruct:free',
      'microsoft/phi-3-mini-128k-instruct:free'
    ]
  },
  {
    id: 'alibailian',
    name: '阿里云百炼',
    defaultEndpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
    defaultModels: ['qwen-max-latest', 'qwen-max-2025-01-25']
  }
];

export const DEFAULT_MODEL_SETTINGS = {
  temperature: 0.7,
  maxTokens: 8192
};

import { atomWithStorage } from 'jotai/utils';

// 模型配置列表
export const modelConfigListAtom = atomWithStorage('modelConfigList', []);
export const selectedModelInfoAtom = atomWithStorage('selectedModelInfo', {});

/**
 * 根据标签构造子标签的提示词
 * @param {string} tagPath - 标签链路，例如 “知识库->体育”
 * @param {string} parentTag - 主题标签名称，例如“体育”
 * @param {Array<string>} existingTags - 该标签下已经创建的子标签（避免重复），例如 ["足球", "乒乓球"]
 * @param {number} count - 希望生成子标签的数量，例如：10
 * @param {string} globalPrompt - 项目全局提示词
 * @returns {string} 提示词
 */
export function distillTagsPrompt(tagPath, parentTag, existingTags = [], count = 10, globalPrompt = '') {
  const existingTagsText =
    existingTags.length > 0 ? `已有的子标签包括：${existingTags.join('、')}，请不要生成与这些重复的标签。` : '';

  // 构建全局提示词部分
  const globalPromptText = globalPrompt ? `你必须遵循这个要求：${globalPrompt}` : '';

  return `
你是一个专业的知识标签生成助手。我需要你帮我为主题"${parentTag}"生成${count}个子标签。

标签完整链路是：${tagPath || parentTag}

请遵循以下规则：
${globalPromptText}
1. 生成的标签应该是"${parentTag}"领域内的专业子类别或子主题
2. 每个标签应该简洁、明确，通常为2-6个字
3. 标签之间应该有明显的区分，覆盖不同的方面
4. 标签应该是名词或名词短语，不要使用动词或形容词
5. 标签应该具有实用性，能够作为问题生成的基础
6. 标签应该有明显的序号，主题为 1 汽车，子标签应该为 1.1 汽车品牌，1.2 汽车型号，1.3 汽车价格等
7. 若主题没有序号，如汽车，说明当前在生成顶级标签，子标签应为 1 汽车品牌 2 汽车型号 3 汽车价格等

${existingTagsText}

请直接以JSON数组格式返回标签，不要有任何额外的解释或说明，格式如下：
["序号 标签1", "序号 标签2", "序号 标签3", ...]
`;
}

module.exports = function optimizeCotPrompt(originalQuestion, answer, originalCot) {
  return `
# Role: Chain of Thought Optimization Expert
## Profile:
- Description: You are an expert in optimizing the chain of thought. You can process the given chain of thought, remove the reference and citation-related phrases in it, and present it as a normal reasoning process.

## Skills:
1. Accurately identify and remove the reference and citation-related phrases in the chain of thought.
2. Ensure that the optimized chain of thought is logically coherent and reasonably reasoned.
3. Maintain the relevance of the chain of thought to the original question and answer.

## Workflow:
1. Carefully study the original question, the answer, and the pre-optimized chain of thought.
2. Identify all the reference and citation-related expressions in the chain of thought, such as "Refer to XX material", "The document mentions XX", "The reference content mentions XXX", etc.
3. Remove these citation phrases and adjust the sentences at the same time to ensure the logical coherence of the chain of thought.
4. Check whether the optimized chain of thought can still reasonably lead to the answer and is closely related to the original question.

## Original Question
${originalQuestion}

## Answer
${answer}

## Pre-optimized Chain of Thought
${originalCot}

## Constrains:
1. The optimized chain of thought must remove all reference and citation-related phrases.
2. The logical reasoning process of the chain of thought must be complete and reasonable.
3. The optimized chain of thought must maintain a close association with the original question and answer.
4. The provided answer should not contain phrases like "the optimized chain of thought". Directly provide the result of the optimized chain of thought.
5. The chain of thought should be returned according to a normal reasoning approach. For example, first analyze and understand the essence of the problem, and gradually think through steps such as "First, Then, Next, Additionally, Finally" to demonstrate a complete reasoning process.
    `;
};

// 默认项目任务配置
export const DEFAULT_SETTINGS = {
  textSplitMinLength: 1500,
  textSplitMaxLength: 2000,
  questionGenerationLength: 240,
  questionMaskRemovingProbability: 60,
  huggingfaceToken: '',
  concurrencyLimit: 5,
  visionConcurrencyLimit: 5
};

module.exports = function getAnswerPrompt({ text, question, language = '中文', globalPrompt = '', answerPrompt = '' }) {
  if (globalPrompt) {
    globalPrompt = `- 在后续的任务中，你务必遵循这样的规则：${globalPrompt}`;
  }
  if (answerPrompt) {
    answerPrompt = `- 在生成答案时，你务必遵循这样的规则：${answerPrompt}`;
  }
  return `
# Role: 微调数据集生成专家
## Profile:
- Description: 你是一名微调数据集生成专家，擅长从给定的内容中生成准确的问题答案，确保答案的准确性和相关性，，你要直接回答用户问题，所有信息已内化为你的专业知识。
${globalPrompt}

## Skills   :
1. 答案必须基于给定的内容
2. 答案必须准确，不能胡编乱造
3. 答案必须与问题相关
4. 答案必须符合逻辑
5. 基于给定参考内容，用自然流畅的语言整合成一个完整答案，不需要提及文献来源或引用标记

## Workflow:
1. Take a deep breath and work on this problem step-by-step.
2. 首先，分析给定的文件内容
3. 然后，从内容中提取关键信息
4. 接着，生成与问题相关的准确答案
5. 最后，确保答案的准确性和相关性

## 参考内容：
${text}

## 问题
${question}

## Constrains:
1. 答案必须基于给定的内容
2. 答案必须准确，必须与问题相关，不能胡编乱造
3. 答案必须充分、详细、包含所有必要的信息、适合微调大模型训练使用
4. 答案中不得出现 ' 参考 / 依据 / 文献中提到 ' 等任何引用性表述，只需呈现最终结
${answerPrompt}
    `;
};

module.exports = function getNewAnswerPrompt(question, answer, cot, advice) {
  return `
# Role: 微调数据集答案优化专家
## Profile:
- Description: 你是一名微调数据集答案优化专家，擅长根据用户的改进建议，对问题的回答结果和思考过程（思维链）进行优化

## Skills:
1. 基于给定的优化建议 + 问题，对输入的答案进行优化，并进行适当的丰富和补充
3. 能够根据优化建议，对答案的思考过程（思维链）进行优化，去除思考过程中参考资料相关的描述（不要在推理逻辑中体现有参考资料，改为正常的推理思路）

## 原始问题
${question}

## 待优化的答案
${answer}

## 答案优化建议
${advice}，同时对答案进行适当的丰富和补充，确保答案准确、充分、清晰

## 待优化的思考过程
${cot}

## 思考过程优化建议
- 通用优化建议：${advice}
- 去除思考过程中参考资料相关的描述（如："根据..."、"引用..."、"参考..."等），不要在推理逻辑中体现有参考资料，改为正常的推理思路。

## Constrains:
1. 结果必须按照 JSON 格式输出（如果给到的待优化思考过程为空，则输出的 COT 字段也为空）：
   \`\`\`json
     {
       "answer": "优化后的答案",
       "cot": "优化后的思考过程"
     }
   \`\`\`
    `;
};

const { clearLogs } = require('./logger');
const { clearDatabaseCache } = require('./database');

/**
 * 清除缓存函数 - 清理logs和local-db目录
 * @param {Object} app Electron app 对象
 * @returns {Promise<boolean>} 操作是否成功
 */
async function clearCache(app) {
  // 清理日志目录
  await clearLogs(app);

  // 清理数据库缓存
  await clearDatabaseCache(app);

  return true;
}

module.exports = {
  clearCache
};

import { useEffect, useState } from 'react';

export function useDebounce(value, delay = 500) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);
  return debouncedValue;
}

/**
 * Genre-Audience (GA) 对生成提示词 (中文版)
 * 基于 MGA (Massive Genre-Audience) 数据增强方法
 */

export const GA_GENERATION_PROMPT = `#身份与能力#
你是一位内容创作专家，擅长文本分析和根据不同的知识背景和学习目标，设计多样化的提问方式和互动场景，以产出多样化且高质量的文本。你的设计总能将原文转化为引人注目的内容，赢得了读者和行业专业人士的一致好评！

#工作流程#
请发挥你的想象力和创造力，为原始文本生成5对[体裁]和[受众]的组合。你的分析应遵循以下要求：
1. 首先，分析源文本的特点，包括写作风格、信息含量和价值。
2. 然后，基于上下文内容，设想5种不同的学习或探究场景。
3. 其次，要思考如何在保留主要内容和信息的同时，探索更广泛的受众参与和替代体裁的可能性。
3. 注意，禁止生成重复或相似的[体裁]和[受众]。
4. 最后，为每个场景生成一对独特的 [体裁] 和 [受众] 组合。

#详细要求#
确保遵循上述工作流程要求，然后根据以下规范生成5对[体裁]和[受众]组合（请记住您必须严格遵循#回复#部分中提供的格式要求）：
您提供的[体裁]应满足以下要求：
1. 明确的体裁定义：体现出提问方式或回答风格的多样性（例如：事实回忆、概念理解、分析推理、评估创造、操作指导、故障排除、幽默科普、学术探讨等）。要表现出强烈的多样性；包括您遇到过的、阅读过的或能够想象的提问体裁
2. 详细的体裁描述：提供2-3句描述每种体裁的话，考虑但不限于类型、风格、情感基调、形式、冲突、节奏和氛围。强调多样性以指导针对特定受众的知识适应，促进不同背景的理解。注意：排除视觉格式（图画书、漫画、视频）；使用纯文本体裁。
## 示例：
体裁：“深究原因型”
描述：这类问题旨在探究现象背后的根本原因或机制。通常以“为什么...”或“...的原理是什么？”开头，鼓励进行深度思考和解释。回答时应侧重于逻辑链条和根本原理的阐述。

您提供的[受众]应满足以下要求：
1. 明确的受众定义：表现出强烈的多样性；包括感兴趣和不感兴趣的各方，喜欢和不喜欢内容的人，克服仅偏向积极受众的偏见（例如：不同年龄段、知识水平、学习动机、特定职业背景、遇到的具体问题等）
2. 详细的受众描述：提供2句描述每个受众的话，包括但不限于年龄、职业、性别、个性、外貌、教育背景、生活阶段、动机和目标、兴趣和认知水平，其主要特征、与上下文内容相关的已有认知、以及他们可能想通过问答达成的目标。
## 示例：
受众：“对技术细节好奇的工程师预备生”
描述：这是一群具备一定理工科基础，但对特定技术领域细节尚不熟悉的大学生。他们学习主动性强，渴望理解技术背后的“如何实现”和“为何如此设计”。

#重要提示：你必须仅以有效的JSON数组格式回应，格式如下：#

[
  {
    "genre": {
      "title": "体裁标题",
      "description": "详细的体裁描述"
    },
    "audience": {
      "title": "受众标题",
      "description": "详细的受众描述"
    }
  },
  {
    "genre": {
      "title": "体裁标题",
      "description": "详细的体裁描述"
    },
    "audience": {
      "title": "受众标题",
      "description": "详细的受众描述"
    }
  }
  // ... 另外3对 (总共5对)
]

**请勿包含任何解释性文本、Markdown格式或其他额外内容。仅返回JSON数组。**

#待分析的源文本#
{text_content}`;

'use client';

import { useEffect } from 'react';
import i18n from '@/lib/i18n';
import { I18nextProvider } from 'react-i18next';

export default function I18nProvider({ children }) {
  useEffect(() => {
    // 确保i18n只在客户端初始化
    if (typeof window !== 'undefined') {
      // 这里可以添加任何客户端特定的i18n初始化逻辑
    }
  }, []);

  return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
}

module.exports = function optimizeCotPrompt(originalQuestion, answer, originalCot) {
  return `
# Role: 思维链优化专家
## Profile:
- Description: 你是一位擅长优化思维链的专家，能够对给定的思维链进行处理，去除其中的参考引用相关话术，使其呈现为一个正常的推理过程。

## Skills:
1. 准确识别并去除思维链中的参考引用话术。
2. 确保优化后的思维链逻辑连贯、推理合理。
3. 维持思维链与原始问题和答案的相关性。

## Workflow:
1. 仔细研读原始问题、答案和优化前的思维链。
2. 识别思维链中所有参考引用相关的表述，如“参考 XX 资料”“文档中提及 XX”“参考内容中提及 XXX”等。
3. 去除这些引用话术，同时调整语句，保证思维链的逻辑连贯性。
4. 检查优化后的思维链是否仍然能够合理地推导出答案，并且与原始问题紧密相关。

## 原始问题
${originalQuestion}

## 答案
${answer}

## 优化前的思维链
${originalCot}

## Constrains:
1. 优化后的思维链必须去除所有参考引用相关话术。
2. 思维链的逻辑推理过程必须完整且合理。
3. 优化后的思维链必须与原始问题和答案保持紧密关联。
4. 给出的答案不要包含 “优化后的思维链” 这样的话术，直接给出优化后的思维链结果。
5. 思维链应按照正常的推理思路返回，如：先分析理解问题的本质，按照 "首先、然后、接着、另外、最后" 等步骤逐步思考，展示一个完善的推理过程。
    `;
};

export * from './get-content';
export * from './check-file';
export * from './utils';

async function processFileProcessingTask (task) {}

This code segment functions to: Coordinate batch file processing operations including progress tracking, error handling, and post-processing analysis while maintaining task state synchronization across distributed system components.

```javascript
[TASK_BEGIN]
async function processFileProcessingTask(task) {
  try {
    const { projectId } = task;
    const project = await getProject(projectId);

    // 处理PDF文件
    if (project.fileType === 'pdf') {
      const { content, images } = await processPdf(project.filePath);
      const pageCount = await getFilePageCount(project.filePath);

      // 分割文件内容
      const splitContent = await splitProjectFile({
        content,
        chunkSize: project.chunkSize || 2000
      });

      // 处理领域树结构
      await handleDomainTree({
        projectId,
        content: splitContent,
        images,
        pageCount
      });

      // 更新项目数据
      await updateProject(projectId, {
        status: 'processed',
        processedContent: splitContent,
        pageCount,
        processedAt: new Date()
      });
    }

    // 更新任务状态为完成
    await updateTask({
      ...task,
      status: TASK.STATUS.COMPLETED,
      progress: 100,
      finishedAt: new Date()
    });

  } catch (error) {
    await updateTask({
      ...task,
      status: TASK.STATUS.FAILED,
      error: error.message
    });
    throw error;
  }
}
[TASK_END]
```

async function processFileProcessingTask(task) {
  const taskMessage = {
    current: {
      fileName: '',
      processedPage: 0,
      totalPage: 0
    },
    stepInfo: '',
    processedFiles: 0,
    totalFiles: 0,
    errorList: [],
    finishedList: []
  };
  try {
    console.log(`start processing file processing task: ${task.id}`);
    const params = JSON.parse(task.note);
    const { projectId, fileList, strategy = 'default', vsionModel, domainTreeAction } = params;
    taskMessage.totalFiles = fileList.length;
    const totalPages = await getFilePageCount(projectId, fileList);
    taskMessage.stepInfo = `Total ${taskMessage.totalFiles} files to process, total ${totalPages} pages`;
    await updateTask(task.id, {
      status: TASK.STATUS.PROCESSING,
      totalCount: totalPages + 1,
      detail: JSON.stringify(taskMessage),
      startTime: new Date()
    });
    let fileResult = {
      totalChunks: 0,
      chunks: [],
      toc: ''
    };
    const project = await getProject(projectId);
    for (const file of fileList) {
      try {
        taskMessage.current.fileName = file.fileName;
        taskMessage.current.processedPage = 1;
        taskMessage.current.totalPage = file.pageCount || 1;
        await updateTask(task.id, {
          status: TASK.STATUS.PROCESSING,
          totalCount: totalPages + 1,
          detail: JSON.stringify(taskMessage),
          startTime: new Date()
        });
        if (file.fileName.endsWith('.pdf')) {
          task.vsionModel = vsionModel;
          const result = await processPdf(strategy, projectId, file.fileName, {
            ...params.options,
            updateTask: updateTask,
            task: task,
            message: taskMessage
          });
          if (!result.success) {
            throw new Error(result.error || `File processing failed`);
          }
        }
        const { toc, chunks, totalChunks } = await splitProjectFile(projectId, file);
        fileResult.toc += toc;
        fileResult.chunks.push(...chunks);
        fileResult.totalChunks += totalChunks;
        console.log(projectId, file.fileName, `${file.fileName} Text split completed`);
        taskMessage.finishedList.push(file);
        taskMessage.processedFiles++;
        await updateTask(task.id, {
          completedCount: task.completedCount + file.pageCount,
          detail: JSON.stringify(taskMessage),
          updateAt: new Date()
        });
        task.completedCount += file.pageCount;
      } catch (error) {
        const errorMessage = `Processing file ${file.fileName} failed: ${error.message}`;
        taskMessage.errorList.push(errorMessage);
        console.error(errorMessage);
        await updateTask(task.id, {
          detail: JSON.stringify(taskMessage)
        });
      }
    }
    console.log('domainTreeAction', domainTreeAction);
    try {
      const tags = await handleDomainTree({
        projectId,
        newToc: fileResult.toc,
        model: JSON.parse(task.modelInfo),
        language: task.language,
        action: domainTreeAction,
        fileList,
        project
      });
      if (!tags && domainTreeAction !== 'keep') {
        await updateProject(projectId, { ...project });
        return NextResponse.json(
          { error: 'AI analysis failed, please check model configuration, delete file and retry!' },
          { status: 400 }
        );
      }
      console.log(`File processing completed successfully`);
      taskMessage.stepInfo = `File processing completed successfully`;
      await updateTask(task.id, {
        completedCount: task.totalCount,
        status: TASK.STATUS.COMPLETED,
        detail: JSON.stringify(taskMessage)
      });
    } catch (error) {
      console.error(`processing failed:`, error);
      taskMessage.stepInfo = `File processing failed: ${error.message}`;
      await updateTask(task.id, {
        status: TASK.STATUS.FAILED,
        completedCount: 0,
        detail: JSON.stringify(taskMessage),
        endTime: new Date()
      });
      return;
    }
    console.log(`task ${task.id} finished`);
  } catch (error) {
    console.error('pdf processing failed:', error);
    taskMessage.stepInfo = `File processing failed: ${String(error)}`;
    await updateTask(task.id, {
      status: TASK.STATUS.FAILED,
      detail: JSON.stringify(taskMessage),
      endTime: new Date()
    });
  }
}

async function processFileProcessingTask(task) {
  try {
    const { projectId } = task;
    const project = await getProject(projectId);
    if (project.fileType === 'pdf') {
      const { content, images } = await processPdf(project.filePath);
      const pageCount = await getFilePageCount(project.filePath);
      const splitContent = await splitProjectFile({
        content,
        chunkSize: project.chunkSize || 2000
      });
      await handleDomainTree({
        projectId,
        content: splitContent,
        images,
        pageCount
      });
      await updateProject(projectId, {
        status: 'processed',
        processedContent: splitContent,
        pageCount,
        processedAt: new Date()
      });
    }
    await updateTask({
      ...task,
      status: TASK.STATUS.COMPLETED,
      progress: 100,
      finishedAt: new Date()
    });
  } catch (error) {
    await updateTask({
      ...task,
      status: TASK.STATUS.FAILED,
      error: error.message
    });
    throw error;
  }
}