| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- import React, { useState, useEffect } from 'react'
- import { Sparkles, ArrowRight, X, RefreshCw, Check } from 'lucide-react'
- import ReactMarkdown from 'react-markdown'
- import { chatService } from '../services/chatService'
- import { useLanguage } from '../contexts/LanguageContext'
- interface AICommandModalProps {
- isOpen: boolean
- onClose: () => void
- context: string
- onApply: (content: string) => void
- authToken: string
- }
- const PRESET_COMMANDS = [
- { label: 'polishContent', valueKey: 'aiCommandInstructPolish' },
- { label: 'expandContent', valueKey: 'aiCommandInstructExpand' },
- { label: 'summarizeContent', valueKey: 'aiCommandInstructSummarize' },
- { label: 'translateToEnglish', valueKey: 'aiCommandInstructTranslateToEn' },
- { label: 'fixGrammar', valueKey: 'aiCommandInstructFixGrammar' },
- ]
- export const AICommandModal: React.FC<AICommandModalProps> = ({ isOpen, onClose, context, onApply, authToken }) => {
- const { t } = useLanguage()
- const [instruction, setInstruction] = useState('')
- const [result, setResult] = useState('')
- const [isGenerating, setIsGenerating] = useState(false)
- const [mode, setMode] = useState<'input' | 'preview'>('input')
- useEffect(() => {
- if (isOpen) {
- setInstruction('')
- setResult('')
- setMode('input')
- setIsGenerating(false)
- }
- }, [isOpen])
- const handleGenerate = async () => {
- if (!instruction) return
- setMode('preview')
- setIsGenerating(true)
- setResult('')
- try {
- const stream = chatService.streamAssist(instruction, context, authToken)
- for await (const chunk of stream) {
- if (chunk.type === 'content') {
- setResult(prev => prev + chunk.data)
- } else if (chunk.type === 'error') {
- setResult(prev => prev + `\n\n[Error: ${chunk.data}]`)
- }
- }
- } catch (error) {
- console.error(error)
- setResult(prev => prev + `\n\n[${t('aiCommandsError')}]`)
- } finally {
- setIsGenerating(false)
- }
- }
- if (!isOpen) return null
- return (
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
- <div className="bg-white rounded-xl shadow-2xl w-full max-w-2xl overflow-hidden flex flex-col max-h-[85vh] animate-in fade-in zoom-in duration-200">
- {/* Header */}
- <div className="bg-gradient-to-r from-purple-600 to-blue-600 p-4 shrink-0 flex justify-between items-center text-white">
- <div className="flex items-center gap-2">
- <Sparkles size={20} className="animate-pulse" />
- <h3 className="font-bold text-lg">{t('aiAssistant')}</h3>
- </div>
- <button onClick={onClose} className="p-1 hover:bg-white/20 rounded-lg transition-colors">
- <X size={20} />
- </button>
- </div>
- {/* Content */}
- <div className="p-6 overflow-y-auto flex-1">
- {mode === 'input' ? (
- <div className="space-y-6">
- <div>
- <label className="block text-sm font-medium text-slate-700 mb-2">{t('aiCommandsModalPreset')}</label>
- <div className="flex flex-wrap gap-2">
- {PRESET_COMMANDS.map(cmd => (
- <button
- key={cmd.label}
- onClick={() => setInstruction(t(cmd.valueKey as any))}
- className={`px-3 py-1.5 text-sm rounded-full border transition-all ${instruction === t(cmd.valueKey as any)
- ? 'bg-purple-100 border-purple-300 text-purple-700'
- : 'bg-white border-slate-200 text-slate-600 hover:border-purple-300 hover:text-purple-600'
- }`}
- >
- {t(cmd.label as keyof typeof t)}
- </button>
- ))}
- </div>
- </div>
- <div>
- <label className="block text-sm font-medium text-slate-700 mb-2">{t('aiCommandsModalCustom')}</label>
- <textarea
- className="w-full h-32 p-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent outline-none resize-none"
- placeholder={t('aiCommandsModalCustomPlaceholder')}
- value={instruction}
- onChange={e => setInstruction(e.target.value)}
- autoFocus
- />
- </div>
- {context && (
- <div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
- <p className="text-xs text-slate-400 mb-1">{t('aiCommandsModalBasedOnSelection')}</p>
- <p className="text-sm text-slate-600 line-clamp-3 font-mono">{context}</p>
- </div>
- )}
- </div>
- ) : (
- <div className="h-full flex flex-col">
- <div className="flex justify-between items-center mb-2">
- <h4 className="font-bold text-slate-700">{t('aiCommandsModalResult')}</h4>
- {isGenerating && (
- <span className="text-xs text-purple-600 flex items-center gap-1">
- <RefreshCw size={12} className="animate-spin" /> {t('aiCommandsGenerating')}
- </span>
- )}
- </div>
- <div className="flex-1 bg-slate-50 border border-slate-200 rounded-lg p-4 overflow-y-auto markdown-body text-sm">
- {result ? <ReactMarkdown>{result}</ReactMarkdown> : <span className="text-slate-400 italic">{t('aiCommandsGenerating')}</span>}
- </div>
- </div>
- )}
- </div>
- {/* Footer */}
- <div className="p-4 border-t border-slate-200 bg-slate-50 shrink-0 flex justify-end gap-3">
- {mode === 'input' ? (
- <button
- onClick={handleGenerate}
- disabled={!instruction}
- className="flex items-center gap-2 px-5 py-2.5 bg-gradient-to-r from-purple-600 to-blue-600 text-white rounded-lg hover:shadow-lg disabled:opacity-50 disabled:shadow-none transition-all font-medium"
- >
- <Sparkles size={16} />
- {t('aiCommandsStartGeneration')}
- </button>
- ) : (
- <>
- <button
- onClick={() => setMode('input')}
- className="px-4 py-2 text-slate-600 hover:bg-slate-200 rounded-lg"
- disabled={isGenerating}
- >
- {t('aiCommandsGoBack')}
- </button>
- <button
- onClick={() => {
- onApply(result)
- onClose()
- }}
- disabled={isGenerating || !result}
- className="flex items-center gap-2 px-5 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 shadow-sm"
- >
- <Check size={16} />
- {t('aiCommandsModalApply')}
- </button>
- </>
- )}
- </div>
- </div>
- </div>
- )
- }
|