| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484 |
- import * as React from 'react';
- import { ChatMessage, AIAssistantSettings } from '../types';
- import { sendChatMessage, createAIChatSession } from '../services/geminiService';
- import { Icon } from './ui/Icon';
- import { Tabs } from './ui/Tabs';
- import { useTranslation } from '../hooks/useI18n';
- const ChatWindow: React.FC = () => {
- const { t } = useTranslation();
- const [messages, setMessages] = React.useState<ChatMessage[]>([]);
- const [userInput, setUserInput] = React.useState('');
- const [isLoading, setIsLoading] = React.useState(false);
- const messagesEndRef = React.useRef<HTMLDivElement>(null);
- React.useEffect(() => {
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
- }, [messages, isLoading]);
- const handleSendMessage = React.useCallback(async () => {
- if (!userInput.trim()) return;
- const userMessage: ChatMessage = { id: `msg-user-${Date.now()}`, sender: 'user', text: userInput };
- setMessages(prev => [...prev, userMessage]);
- const messageToSend = userInput;
- setUserInput('');
- setIsLoading(true);
- const aiResponse = await sendChatMessage(messageToSend);
- setMessages(prev => [...prev, aiResponse]);
- setIsLoading(false);
- }, [userInput]);
- return (
- <div className="flex flex-col h-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
- <div className="p-4 border-b border-gray-200 dark:border-gray-700">
- <h3 className="font-semibold text-lg text-gray-900 dark:text-white">{t('ai_assistant.test_title')}</h3>
- </div>
- <div className="flex-1 p-4 overflow-y-auto space-y-4">
- {messages.map(msg => (
- <div key={msg.id} className={`flex items-start gap-3 ${msg.sender === 'user' ? 'justify-end' : ''}`}>
- {msg.sender === 'ai' && (
- <div className="w-8 h-8 rounded-full bg-brand-primary/20 text-brand-primary flex items-center justify-center flex-shrink-0">
- <Icon className="h-5 w-5"><path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /></Icon>
- </div>
- )}
- <div className={`max-w-md p-3 rounded-lg ${msg.sender === 'user' ? 'bg-blue-600 text-white rounded-br-none' : 'bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-bl-none'}`}>
- <p className="text-sm">{msg.text}</p>
- </div>
- {msg.sender === 'user' && (
- <div className="w-8 h-8 rounded-full bg-gray-500 dark:bg-gray-600 flex items-center justify-center flex-shrink-0 text-white">
- <Icon className="h-5 w-5"><path strokeLinecap="round" strokeLinejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></Icon>
- </div>
- )}
- </div>
- ))}
- {isLoading && (
- <div className="flex items-start gap-3">
- <div className="w-8 h-8 rounded-full bg-brand-primary/20 text-brand-primary flex items-center justify-center flex-shrink-0">
- <Icon className="h-5 w-5"><path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /></Icon>
- </div>
- <div className="max-w-md p-3 rounded-lg bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-bl-none">
- <div className="flex items-center space-x-1">
- <span className="w-2 h-2 bg-gray-400 dark:bg-gray-400 rounded-full animate-pulse delay-0"></span>
- <span className="w-2 h-2 bg-gray-400 dark:bg-gray-400 rounded-full animate-pulse delay-150"></span>
- <span className="w-2 h-2 bg-gray-400 dark:bg-gray-400 rounded-full animate-pulse delay-300"></span>
- </div>
- </div>
- </div>
- )}
- <div ref={messagesEndRef} />
- </div>
- <div className="p-4 border-t border-gray-200 dark:border-gray-700 flex items-center gap-3">
- <input
- type="text"
- value={userInput}
- onChange={(e) => setUserInput(e.target.value)}
- onKeyPress={(e) => e.key === 'Enter' && !isLoading && handleSendMessage()}
- placeholder="Type your message..."
- className="flex-1 bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white p-2 rounded-md border-2 border-gray-300 dark:border-gray-600 focus:outline-none focus:border-brand-primary focus:ring-1 focus:ring-brand-primary/50"
- disabled={isLoading}
- />
- <button onClick={handleSendMessage} disabled={isLoading} className="px-4 py-2 bg-brand-primary text-white font-semibold rounded-md hover:bg-brand-secondary disabled:bg-gray-500 transition-colors">
- Send
- </button>
- </div>
- </div>
- );
- };
- const sectionClasses = "bg-white dark:bg-gray-800/50 p-6 rounded-lg border border-gray-200 dark:border-gray-700/50";
- const labelClasses = "block text-lg font-semibold text-gray-900 dark:text-white";
- const descriptionClasses = "text-sm text-gray-500 dark:text-gray-400 mt-1 mb-4";
- const inputBaseClasses = "w-full text-gray-900 dark:text-white p-3 rounded-md border focus:outline-none focus:border-brand-primary focus:ring-0 transition-colors";
- const inputBgClasses = "bg-gray-100/50 dark:bg-black/20 border-gray-300 dark:border-white/10";
- const tagColors = [
- { bg: 'bg-red-500', text: 'text-white', closeButton: 'text-red-100 hover:text-white' },
- { bg: 'bg-blue-600', text: 'text-white', closeButton: 'text-blue-100 hover:text-white' },
- { bg: 'bg-green-600', text: 'text-white', closeButton: 'text-green-100 hover:text-white' },
- { bg: 'bg-yellow-500', text: 'text-black', closeButton: 'text-yellow-800 hover:text-black' },
- { bg: 'bg-purple-600', text: 'text-white', closeButton: 'text-purple-100 hover:text-white' },
- { bg: 'bg-indigo-600', text: 'text-white', closeButton: 'text-indigo-100 hover:text-white' },
- { bg: 'bg-pink-600', text: 'text-white', closeButton: 'text-pink-100 hover:text-white' },
- { bg: 'bg-teal-500', text: 'text-white', closeButton: 'text-teal-100 hover:text-white' },
- ];
- const getColorForKeyword = (keyword: string) => {
- let hash = 0;
- for (let i = 0; i < keyword.length; i++) {
- hash = keyword.charCodeAt(i) + ((hash << 5) - hash);
- hash = hash & hash;
- }
- const index = Math.abs(hash % tagColors.length);
- return tagColors[index];
- };
- const KeywordInput: React.FC<{
- label: string;
- description: string;
- keywords: string[];
- onAdd: (keyword: string) => void;
- onRemove: (keyword: string) => void;
- }> = ({ label, description, keywords, onAdd, onRemove }) => {
- const { t } = useTranslation();
- const [newKeyword, setNewKeyword] = React.useState('');
- const handleAdd = () => {
- if (newKeyword.trim() && !keywords.includes(newKeyword.trim())) {
- onAdd(newKeyword.trim());
- setNewKeyword('');
- }
- };
- return (
- <div className={sectionClasses}>
- <label className={labelClasses}>{label}</label>
- <p className={descriptionClasses}>{description}</p>
- <div className="flex items-center gap-2">
- <input
- type="text"
- value={newKeyword}
- onChange={(e) => setNewKeyword(e.target.value)}
- onKeyPress={(e) => e.key === 'Enter' && handleAdd()}
- className={`${inputBaseClasses} ${inputBgClasses}`}
- placeholder={`${t('add')} a keyword...`}
- />
- <button onClick={handleAdd} className="px-4 py-2 bg-gray-200 dark:bg-gray-600 text-sm font-semibold rounded-md hover:bg-gray-300 dark:hover:bg-gray-500">{t('add')}</button>
- </div>
- <div className="flex flex-wrap gap-3 mt-4 min-h-[3rem]">
- {keywords.map(kw => {
- const color = getColorForKeyword(kw);
- return (
- <div key={kw} className={`flex items-center gap-2 ${color.bg} ${color.text} text-base font-bold px-4 py-2 rounded-full`}>
- <span>{kw}</span>
- <button onClick={() => onRemove(kw)} className={`${color.closeButton} text-lg font-bold leading-none transition-colors`}>×</button>
- </div>
- );
- })}
- </div>
- </div>
- );
- };
- const KnowledgeBaseModal: React.FC<{
- onClose: () => void;
- onAddFiles: (files: File[]) => void;
- onAddUrl: (url: string) => void;
- }> = ({ onClose, onAddFiles, onAddUrl }) => {
- const { t } = useTranslation();
- const [activeTab, setActiveTab] = React.useState<'files' | 'web'>('files');
- const [newUrl, setNewUrl] = React.useState('');
- const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- if (e.target.files) {
- onAddFiles(Array.from(e.target.files));
- onClose();
- }
- };
- const handleAddUrl = () => {
- if (newUrl.trim()) {
- try {
- new URL(newUrl.trim());
- onAddUrl(newUrl.trim());
- onClose();
- } catch (_) {
- alert('Please enter a valid URL.');
- }
- }
- };
- return (
- <div className="fixed inset-0 bg-black/70 z-50 flex items-center justify-center backdrop-blur-sm" onClick={onClose}>
- <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-2xl border border-gray-200 dark:border-gray-700 flex flex-col" onClick={e => e.stopPropagation()}>
- <div className="p-6 border-b border-gray-200 dark:border-gray-700">
- <h2 className="text-2xl font-bold text-gray-900 dark:text-white">{t('ai_assistant.add_knowledge')}</h2>
- </div>
- <div className="p-6">
- <Tabs tabs={['files', 'web']} activeTab={activeTab} onTabClick={(t) => setActiveTab(t as 'files' | 'web')} />
- </div>
- <div className="p-6 pt-0 flex-1">
- {activeTab === 'files' ? (
- <div className="mt-1 flex justify-center px-6 pt-10 pb-10 border-2 border-gray-300 dark:border-gray-600 border-dashed rounded-md h-full">
- <div className="space-y-1 text-center">
- <Icon className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"><path d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></Icon>
- <div className="flex text-sm text-gray-500 dark:text-gray-400">
- <label htmlFor="file-upload" className="relative cursor-pointer bg-white dark:bg-gray-800 rounded-md font-medium text-brand-primary hover:text-brand-secondary focus-within:outline-none">
- <span>Upload files</span>
- <input id="file-upload" name="file-upload" type="file" multiple className="sr-only" onChange={handleFileChange} />
- </label>
- <p className="pl-1">or drag and drop</p>
- </div>
- <p className="text-xs text-gray-400 dark:text-gray-500">PDF, TXT, DOCX up to 10MB</p>
- </div>
- </div>
- ) : (
- <div>
- <label htmlFor="url-input" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Add from Web</label>
- <div className="flex items-center gap-2">
- <input id="url-input" type="url" value={newUrl} onChange={e => setNewUrl(e.target.value)} onKeyPress={e => e.key === 'Enter' && handleAddUrl()}
- className={`${inputBaseClasses} ${inputBgClasses}`}
- placeholder="https://example.com/about" />
- <button onClick={handleAddUrl} className="px-4 py-2 bg-gray-200 dark:bg-gray-600 text-sm font-semibold rounded-md hover:bg-gray-300 dark:hover:bg-gray-500">{t('add')} URL</button>
- </div>
- </div>
- )}
- </div>
- </div>
- </div>
- );
- };
- interface EditorProps {
- settings: AIAssistantSettings;
- updateSettings: (updates: Partial<AIAssistantSettings>) => void;
- }
- const PersonaEditor: React.FC<EditorProps> = ({ settings, updateSettings }) => {
- const { t } = useTranslation();
- return (
- <div className="space-y-8">
- <div className={sectionClasses}>
- <label htmlFor="persona" className={labelClasses}>{t('ai_assistant.system_instruction')}</label>
- <p className={descriptionClasses}>{t('ai_assistant.system_instruction_desc')}</p>
- <textarea id="persona" rows={6} value={settings.persona} onChange={e => updateSettings({ persona: e.target.value })}
- className={`${inputBaseClasses} ${inputBgClasses}`}
- placeholder="e.g., You are a witty pirate captain who gives business advice."/>
- </div>
- <div className={sectionClasses}>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6">
- <div>
- <label htmlFor="voice" className={labelClasses}>{t('ai_assistant.voice')}</label>
- <p className={`${descriptionClasses} mb-2`}>{t('ai_assistant.voice_desc')}</p>
- <div className="flex items-center gap-2">
- <select id="voice" value={settings.voiceId} onChange={e => updateSettings({ voiceId: e.target.value })}
- className={`${inputBaseClasses} ${inputBgClasses}`}>
- <option value="vo1">Alloy</option><option value="vo2">Echo</option><option value="vo3">Fable</option>
- </select>
- <button className="p-3 rounded-md bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500">
- <Icon className="h-5 w-5"><path strokeLinecap="round" strokeLinejoin="round" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.108 12 5v14c0 .892-1.077 1.337-1.707.707L5.586 15z" /></Icon>
- </button>
- </div>
- </div>
- <div>
- <label className={labelClasses}>{t('ai_assistant.clone')}</label>
- <p className={`${descriptionClasses} mb-2`}>{t('ai_assistant.clone_desc')}</p>
- <button disabled className="w-full p-3 rounded-md bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-400 cursor-not-allowed flex items-center justify-center gap-2">
- {t('ai_assistant.clone_voice')} <span className="text-xs bg-yellow-400 text-yellow-900 px-2 py-0.5 rounded-full font-bold">{t('pro')}</span>
- </button>
- </div>
- <div>
- <label htmlFor="language" className={labelClasses}>{t('ai_assistant.language')}</label>
- <p className={`${descriptionClasses} mb-2`}>{t('ai_assistant.language_desc')}</p>
- <select id="language" value={settings.language} onChange={e => updateSettings({ language: e.target.value })}
- className={`${inputBaseClasses} ${inputBgClasses}`}>
- <option>English</option>
- <option>Japanese</option>
- <option>Chinese</option>
- <option>Korean</option>
- </select>
- </div>
- <div>
- <label className={labelClasses}>{t('ai_assistant.conversation_style')}</label>
- <p className={`${descriptionClasses} mb-2`}>{t('ai_assistant.conversation_style_desc')}</p>
- <div className="flex items-center gap-1 bg-gray-100/50 dark:bg-black/20 p-1 rounded-lg border border-gray-300 dark:border-white/10">
- {(['friendly', 'professional', 'witty'] as const).map(style => (
- <button key={style} onClick={() => updateSettings({ conversationStyle: style })}
- className={`flex-1 px-3 py-2 text-sm rounded-md capitalize transition-colors ${settings.conversationStyle === style ? 'bg-brand-primary text-white shadow' : 'hover:bg-gray-200 dark:hover:bg-gray-600'}`}>
- {style}
- </button>
- ))}
- </div>
- </div>
- </div>
- </div>
- </div>
- );
- };
- const KnowledgeEditor: React.FC<EditorProps & { onOpenModal: () => void }> = ({ settings, updateSettings, onOpenModal }) => {
- const { t } = useTranslation();
- const handleRemoveFile = (fileName: string) => {
- updateSettings({ knowledgeBaseFiles: settings.knowledgeBaseFiles.filter(f => f.name !== fileName) });
- };
- const handleRemoveUrl = (urlToRemove: string) => {
- updateSettings({ knowledgeBaseUrls: settings.knowledgeBaseUrls.filter(url => url !== urlToRemove) });
- };
-
- const hasContent = settings.knowledgeBaseFiles.length > 0 || settings.knowledgeBaseUrls.length > 0;
- return (
- <div className="space-y-8">
- <div>
- <button onClick={onOpenModal} className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-brand-primary text-white font-semibold rounded-md hover:bg-brand-secondary transition-colors">
- <Icon className="h-5 w-5"><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /></Icon>
- {t('ai_assistant.add_knowledge')}
- </button>
- </div>
- <div className={sectionClasses}>
- {!hasContent ? (
- <div className="text-center py-10">
- <Icon className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"><path strokeLinecap="round" strokeLinejoin="round" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7l8-4 8 4m-8-4v14" /></Icon>
- <h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">{t('ai_assistant.no_knowledge')}</h3>
- <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{t('ai_assistant.no_knowledge_desc')}</p>
- </div>
- ) : (
- <div className="space-y-3">
- {settings.knowledgeBaseUrls.map(url => (
- <div key={url} className="flex items-center justify-between px-4 py-4 bg-gray-100 dark:bg-gray-900/50 rounded-lg">
- <div className="flex items-center gap-3 min-w-0">
- <Icon className="h-6 w-6 text-gray-500 dark:text-gray-400 flex-shrink-0"><path strokeLinecap="round" strokeLinejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /></Icon>
- <a href={url} target="_blank" rel="noopener noreferrer" className="text-sm font-medium text-gray-900 dark:text-white truncate hover:underline">{url}</a>
- </div>
- <button onClick={() => handleRemoveUrl(url)} className="text-gray-400 dark:text-gray-500 hover:text-white p-1 rounded-full">
- <Icon className="h-5 w-5"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /></Icon>
- </button>
- </div>
- ))}
- {(settings.knowledgeBaseFiles.length > 0 && settings.knowledgeBaseUrls.length > 0) && (
- <hr className="border-gray-200 dark:border-gray-700" />
- )}
- {settings.knowledgeBaseFiles.map(file => (
- <div key={file.name} className="flex items-center justify-between px-4 py-4 bg-gray-100 dark:bg-gray-900/50 rounded-lg">
- <div className="flex items-center gap-3 min-w-0">
- <Icon className="h-6 w-6 text-gray-500 dark:text-gray-400 flex-shrink-0"><path strokeLinecap="round" strokeLinejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></Icon>
- <span className="text-sm font-medium text-gray-900 dark:text-white truncate">{file.name}</span>
- <span className="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">({(file.size / 1024).toFixed(1)} KB)</span>
- </div>
- <button onClick={() => handleRemoveFile(file.name)} className="text-gray-400 dark:text-gray-500 hover:text-white p-1 rounded-full">
- <Icon className="h-5 w-5"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /></Icon>
- </button>
- </div>
- ))}
- </div>
- )}
- </div>
- </div>
- );
- };
- const SensitivityEditor: React.FC<EditorProps> = ({ settings, updateSettings }) => {
- const { t } = useTranslation();
- return (
- <div className="space-y-8">
- <KeywordInput
- label={t('ai_assistant.forbidden_user')}
- description={t('ai_assistant.forbidden_user_desc')}
- keywords={settings.forbiddenUserKeywords}
- onAdd={(kw) => updateSettings({ forbiddenUserKeywords: [...settings.forbiddenUserKeywords, kw] })}
- onRemove={(kw) => updateSettings({ forbiddenUserKeywords: settings.forbiddenUserKeywords.filter(k => k !== kw) })}
- />
- <KeywordInput
- label={t('ai_assistant.forbidden_ai')}
- description={t('ai_assistant.forbidden_ai_desc')}
- keywords={settings.forbiddenAIKeywords}
- onAdd={(kw) => updateSettings({ forbiddenAIKeywords: [...settings.forbiddenAIKeywords, kw] })}
- onRemove={(kw) => updateSettings({ forbiddenAIKeywords: settings.forbiddenAIKeywords.filter(k => k !== kw) })}
- />
- </div>
- );
- };
- interface AIAssistantProps {
- aiAssistantSettings: AIAssistantSettings;
- onUpdateSettings: (newSettings: AIAssistantSettings) => void;
- initialTab: 'persona' | 'knowledge' | 'sensitivity';
- }
- const AIAssistant: React.FC<AIAssistantProps> = ({ aiAssistantSettings, onUpdateSettings, initialTab }) => {
- const { t } = useTranslation();
- const [isKnowledgeModalOpen, setIsKnowledgeModalOpen] = React.useState(false);
- React.useEffect(() => {
- createAIChatSession(aiAssistantSettings.persona);
- }, [aiAssistantSettings.persona]);
- const updateSettings = (updates: Partial<AIAssistantSettings>) => {
- onUpdateSettings({ ...aiAssistantSettings, ...updates });
- };
- const handleAddFiles = (files: File[]) => {
- const newFiles = files.map(file => ({
- name: file.name,
- size: file.size,
- type: file.type,
- }));
- updateSettings({ knowledgeBaseFiles: [...aiAssistantSettings.knowledgeBaseFiles, ...newFiles] });
- };
- const handleAddUrl = (newUrl: string) => {
- if (!aiAssistantSettings.knowledgeBaseUrls.includes(newUrl)) {
- updateSettings({ knowledgeBaseUrls: [...aiAssistantSettings.knowledgeBaseUrls, newUrl] });
- }
- };
-
- const handleSaveChanges = () => {
- createAIChatSession(aiAssistantSettings.persona);
- alert(t('ai_assistant.save_success'));
- };
-
- const renderEditor = () => {
- switch (initialTab) {
- case 'persona':
- return <PersonaEditor settings={aiAssistantSettings} updateSettings={updateSettings} />;
- case 'knowledge':
- return <KnowledgeEditor settings={aiAssistantSettings} updateSettings={updateSettings} onOpenModal={() => setIsKnowledgeModalOpen(true)} />;
- case 'sensitivity':
- return <SensitivityEditor settings={aiAssistantSettings} updateSettings={updateSettings} />;
- default:
- return <PersonaEditor settings={aiAssistantSettings} updateSettings={updateSettings} />;
- }
- };
-
- const pageTitles = {
- persona: t('nav.ai_assistant.persona'),
- knowledge: t('nav.ai_assistant.knowledge'),
- sensitivity: t('nav.ai_assistant.sensitivity')
- };
- return (
- <>
- {isKnowledgeModalOpen && (
- <KnowledgeBaseModal
- onClose={() => setIsKnowledgeModalOpen(false)}
- onAddFiles={handleAddFiles}
- onAddUrl={handleAddUrl}
- />
- )}
- <div className="flex h-full p-8 gap-8">
- <div className="flex-1 flex flex-col min-w-0">
- <header className="flex justify-between items-center flex-shrink-0 mb-8">
- <div>
- <h2 className="text-3xl font-bold text-gray-900 dark:text-white">{t('ai_assistant.title')} / {pageTitles[initialTab]}</h2>
- <p className="text-gray-500 dark:text-gray-400 mt-1">{t('ai_assistant.subtitle')}</p>
- </div>
- {initialTab === 'persona' && (
- <button onClick={handleSaveChanges} className="bg-brand-primary text-white font-bold py-2 px-5 rounded-md hover:bg-brand-secondary transition-colors">
- {t('ai_assistant.save_changes')}
- </button>
- )}
- </header>
- <div className="overflow-y-auto flex-1 -mr-4 pr-4">
- {renderEditor()}
- </div>
- </div>
- <aside className="w-[450px] flex-shrink-0">
- <div className="sticky top-8">
- <ChatWindow />
- </div>
- </aside>
- </div>
- </>
- );
- };
- export default AIAssistant;
|