ImportFolderDrawer.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import React, { useState, useEffect } from 'react';
  2. import { X, Calendar, Clock, FolderInput, ArrowRight } from 'lucide-react';
  3. import { useLanguage } from '../contexts/LanguageContext';
  4. import { ModelConfig, ModelType, IndexingConfig } from '../types';
  5. import { modelConfigService } from '../services/modelConfigService';
  6. import { importService } from '../services/importService';
  7. import { useToast } from '../contexts/ToastContext';
  8. import IndexingModalWithMode from './IndexingModalWithMode';
  9. interface ImportFolderDrawerProps {
  10. isOpen: boolean;
  11. onClose: () => void;
  12. authToken: string;
  13. initialGroupId?: string; // If provided, locks target to this group
  14. initialGroupName?: string;
  15. onImportSuccess?: () => void;
  16. }
  17. export const ImportFolderDrawer: React.FC<ImportFolderDrawerProps> = ({
  18. isOpen,
  19. onClose,
  20. authToken,
  21. initialGroupId,
  22. initialGroupName,
  23. onImportSuccess
  24. }) => {
  25. const { t } = useLanguage();
  26. const { showError, showSuccess } = useToast();
  27. // Form State
  28. const [sourcePath, setSourcePath] = useState('');
  29. const [targetName, setTargetName] = useState('');
  30. const [executionType, setExecutionType] = useState<'immediate' | 'scheduled'>('immediate');
  31. const [scheduledTime, setScheduledTime] = useState('');
  32. // Indexing Config State
  33. const [isIndexingConfigOpen, setIsIndexingConfigOpen] = useState(false);
  34. // Data State
  35. const [models, setModels] = useState<ModelConfig[]>([]);
  36. const [isLoading, setIsLoading] = useState(false);
  37. useEffect(() => {
  38. if (isOpen) {
  39. // Reset form
  40. setSourcePath('');
  41. setTargetName(initialGroupName || '');
  42. setExecutionType('immediate');
  43. setScheduledTime('');
  44. setIsIndexingConfigOpen(false);
  45. // Fetch models
  46. modelConfigService.getAll(authToken).then(res => {
  47. setModels(res.filter(m => m.type === ModelType.EMBEDDING));
  48. });
  49. }
  50. }, [isOpen, authToken, initialGroupName]);
  51. // Auto-fill target name from path if not locked to a group
  52. const handlePathChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  53. const val = e.target.value;
  54. setSourcePath(val);
  55. if (!initialGroupId && val) {
  56. // Extract last folder name (Windows or Unix style)
  57. const parts = val.split(/[\\/]/).filter(p => p);
  58. if (parts.length > 0) {
  59. setTargetName(parts[parts.length - 1]);
  60. }
  61. }
  62. };
  63. const handleNext = async () => {
  64. if (!sourcePath) {
  65. showError(t('fillSourcePath'));
  66. return;
  67. }
  68. if (!initialGroupId && !targetName) {
  69. showError(t('fillTargetName'));
  70. return;
  71. }
  72. if (executionType === 'scheduled' && !scheduledTime) {
  73. showError(t('selectExecTime'));
  74. return;
  75. }
  76. // Open indexing config modal
  77. setIsIndexingConfigOpen(true);
  78. };
  79. const handleConfirmConfig = async (config: IndexingConfig) => {
  80. setIsLoading(true);
  81. try {
  82. await importService.create(authToken, {
  83. sourcePath,
  84. targetGroupId: initialGroupId || undefined,
  85. targetGroupName: initialGroupId ? undefined : targetName,
  86. embeddingModelId: config.embeddingModelId,
  87. scheduledAt: executionType === 'scheduled' ? new Date(scheduledTime).toISOString() : undefined,
  88. chunkSize: config.chunkSize,
  89. chunkOverlap: config.chunkOverlap,
  90. mode: config.mode
  91. });
  92. showSuccess(executionType === 'immediate' ? t('importTaskStarted') : t('importTaskScheduled'));
  93. onImportSuccess?.();
  94. onClose();
  95. } catch (error: any) {
  96. showError(t('submitFailed', error.message));
  97. } finally {
  98. setIsLoading(false);
  99. setIsIndexingConfigOpen(false);
  100. }
  101. };
  102. if (!isOpen) return null;
  103. return (
  104. <>
  105. <div className="fixed inset-0 z-50 flex justify-end">
  106. <div className="absolute inset-0 bg-black/20 backdrop-blur-sm transition-opacity" onClick={onClose} />
  107. <div className="relative w-full max-w-md bg-white shadow-2xl flex flex-col h-full animate-in slide-in-from-right duration-300">
  108. {/* Header */}
  109. <div className="px-6 py-4 border-b border-slate-100 flex items-center justify-between shrink-0 bg-white">
  110. <h2 className="text-lg font-bold text-slate-800 flex items-center gap-2">
  111. <FolderInput className="w-5 h-5 text-blue-600" />
  112. {t('importFolderTitle')}
  113. </h2>
  114. <button onClick={onClose} className="p-2 -mr-2 text-slate-400 hover:text-slate-600 hover:bg-slate-50 rounded-full transition-colors">
  115. <X size={20} />
  116. </button>
  117. </div>
  118. {/* Body */}
  119. <div className="flex-1 overflow-y-auto p-6 space-y-6">
  120. <div className="bg-blue-50 border border-blue-100 rounded-lg p-3 text-sm text-blue-800">
  121. {t('importFolderTip')}
  122. </div>
  123. {/* Source Path */}
  124. <div className="space-y-2">
  125. <label className="text-sm font-medium text-slate-700">{t('lblSourcePath')}</label>
  126. <input
  127. type="text"
  128. value={sourcePath}
  129. onChange={handlePathChange}
  130. placeholder="例如: D:/data/courses/gen-ai"
  131. className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
  132. />
  133. </div>
  134. {/* Target Group */}
  135. <div className="space-y-2">
  136. <label className="text-sm font-medium text-slate-700">{t('lblTargetGroup')}</label>
  137. <input
  138. type="text"
  139. value={targetName}
  140. onChange={e => setTargetName(e.target.value)}
  141. disabled={!!initialGroupId} // Readonly if locking to group
  142. placeholder={t('placeholderNewGroup')}
  143. className={`w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none ${initialGroupId ? 'bg-slate-50 text-slate-500' : ''}`}
  144. />
  145. {initialGroupId && <p className="text-xs text-slate-400">{t('importToCurrentGroup')}</p>}
  146. </div>
  147. {/* Execution Type */}
  148. <div className="space-y-3 pt-4 border-t border-slate-100">
  149. <label className="text-sm font-medium text-slate-700">{t('lblExecType')}</label>
  150. <div className="flex gap-4">
  151. <label className="flex items-center gap-2 cursor-pointer">
  152. <input
  153. type="radio"
  154. name="executionType"
  155. value="immediate"
  156. checked={executionType === 'immediate'}
  157. onChange={() => setExecutionType('immediate')}
  158. className="text-blue-600 focus:ring-blue-500"
  159. />
  160. <span className="text-sm text-slate-700">{t('execImmediate')}</span>
  161. </label>
  162. <label className="flex items-center gap-2 cursor-pointer">
  163. <input
  164. type="radio"
  165. name="executionType"
  166. value="scheduled"
  167. checked={executionType === 'scheduled'}
  168. onChange={() => setExecutionType('scheduled')}
  169. className="text-blue-600 focus:ring-blue-500"
  170. />
  171. <span className="text-sm text-slate-700">{t('execScheduled')}</span>
  172. </label>
  173. </div>
  174. </div>
  175. {/* Schedule Time Picker */}
  176. {executionType === 'scheduled' && (
  177. <div className="space-y-2 animate-in fade-in slide-in-from-top-2">
  178. <label className="text-sm font-medium text-slate-700 flex items-center gap-2">
  179. <Clock size={16} />
  180. {t('lblStartTime')}
  181. </label>
  182. <input
  183. type="datetime-local"
  184. value={scheduledTime}
  185. onChange={e => setScheduledTime(e.target.value)}
  186. min={new Date().toISOString().slice(0, 16)}
  187. className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none"
  188. />
  189. </div>
  190. )}
  191. </div>
  192. {/* Footer */}
  193. <div className="p-6 border-t border-slate-100 shrink-0 flex gap-3 bg-slate-50">
  194. <button
  195. onClick={onClose}
  196. className="flex-1 px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-200 rounded-lg transition-colors"
  197. >
  198. {t('cancel')}
  199. </button>
  200. <button
  201. onClick={handleNext}
  202. className="flex-1 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg shadow-sm flex justify-center items-center gap-2 transition-all"
  203. >
  204. <span>{t('nextStep')}</span>
  205. <ArrowRight size={16} />
  206. </button>
  207. </div>
  208. </div>
  209. </div>
  210. {/* Indexing Config Modal */}
  211. <IndexingModalWithMode
  212. isOpen={isIndexingConfigOpen}
  213. onClose={() => setIsIndexingConfigOpen(false)}
  214. files={[]} // Empty array for folder import mode
  215. embeddingModels={models}
  216. defaultEmbeddingId={models.length > 0 ? models[0].id : ''}
  217. onConfirm={handleConfirmConfig}
  218. isReconfiguring={false}
  219. />
  220. </>
  221. );
  222. };