| |
| |
| |
| |
|
|
|
|
| document.addEventListener('DOMContentLoaded', () => {
|
|
|
| const ui = {
|
| sidebar: document.getElementById('sidebar'),
|
| menuButton: document.getElementById('menu-button'),
|
| chatWindow: document.getElementById('chat-window'),
|
| chatForm: document.getElementById('chat-form'),
|
| messageInput: document.getElementById('message-input'),
|
| sendButton: document.getElementById('send-button'),
|
| sessionList: document.getElementById('session-list'),
|
| newChatButton: document.getElementById('new-chat-button'),
|
| toggleSettingsButton: document.getElementById('toggle-settings-button'),
|
| settingsPanel: document.getElementById('settings-panel'),
|
| personalityInput: document.getElementById('personality-input'),
|
| savePersonalityButton: document.getElementById('save-personality-button'),
|
| fileInput: document.getElementById('file-input'),
|
| filePreviewContainer: document.getElementById('file-preview-container'),
|
| fileNameSpan: document.getElementById('file-name'),
|
| removeFileButton: document.getElementById('remove-file-button'),
|
| chatTitle: document.getElementById('chat-title'),
|
| };
|
|
|
|
|
| let state = {
|
| currentSessionId: localStorage.getItem('active_chat_session_id') || null,
|
| attachedFile: null,
|
| isLoading: false,
|
| };
|
|
|
|
|
| const api = {
|
| getSessions: () => fetch('/api/sessions').then(res => res.json()),
|
| getSession: (id) => fetch(`/api/session/${id}`).then(res => res.json()),
|
| deleteSession: (id) => fetch(`/api/session/${id}`, { method: 'DELETE' }),
|
| renameSession: (id, title) => fetch(`/api/session/${id}/rename`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }) }),
|
| pinSession: (id) => fetch(`/api/session/${id}/pin`, { method: 'POST' }),
|
| setPersonality: (personality) => fetch('/api/set_personality', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ personality }) }),
|
| postChat: (formData) => fetch('/api/chat', { method: 'POST', body: formData }).then(res => res.json()),
|
| };
|
|
|
|
|
| const render = {
|
| sessions: (sessions) => {
|
| ui.sessionList.innerHTML = '';
|
| sessions.forEach(session => {
|
| const item = document.createElement('div');
|
| item.className = 'session-item';
|
| item.dataset.sessionId = session.id;
|
| item.innerHTML = `
|
| <span class="session-title">${session.title}</span>
|
| <div class="session-actions">
|
| <button class="session-action-button pin-button ${session.pinned ? 'pinned' : ''}" title="ピン留め">📌</button>
|
| <button class="session-action-button rename-button" title="名前を変更">✏️</button>
|
| <button class="session-action-button delete-button" title="削除">🗑️</button>
|
| </div>`;
|
| ui.sessionList.appendChild(item);
|
| });
|
| render.activeSessionHighlight();
|
| },
|
| chatHistory: (history) => {
|
| ui.chatWindow.innerHTML = '';
|
| history.forEach(msg => {
|
| const content = {
|
| text: msg.parts.filter(p => !p.startsWith('/uploads/')).join(' '),
|
| imageUrl: msg.parts.find(p => p.startsWith('/uploads/'))
|
| };
|
| render.message({ sender: msg.role === 'user' ? 'user' : 'bot', ...content });
|
| });
|
| },
|
| message: ({ sender, text, imageFile, imageUrl }) => {
|
| const el = document.createElement('div');
|
| el.className = `chat-message ${sender}-message`;
|
| const imageSrc = imageUrl || (imageFile ? URL.createObjectURL(imageFile) : null);
|
| if (imageSrc) {
|
| const img = document.createElement('img');
|
| img.src = imageSrc;
|
| img.className = 'message-image';
|
| el.prepend(img);
|
| }
|
| if (text) {
|
| const textEl = document.createElement('div');
|
| textEl.innerHTML = DOMPurify.sanitize(marked.parse(text));
|
| el.appendChild(textEl);
|
| }
|
| ui.chatWindow.appendChild(el);
|
| ui.chatWindow.scrollTop = ui.chatWindow.scrollHeight;
|
| return el;
|
| },
|
| activeSessionHighlight: () => {
|
| document.querySelectorAll('.session-item').forEach(item => {
|
| item.classList.toggle('active', item.dataset.sessionId === state.currentSessionId);
|
| });
|
| },
|
| filePreview: () => {
|
| if (state.attachedFile) {
|
| ui.fileNameSpan.textContent = state.attachedFile.name;
|
| ui.filePreviewContainer.classList.remove('hidden');
|
| } else {
|
| ui.filePreviewContainer.classList.add('hidden');
|
| ui.fileInput.value = '';
|
| }
|
| },
|
| };
|
|
|
|
|
| const actions = {
|
| startNewChat: async () => {
|
| state.currentSessionId = `session-${Date.now()}`;
|
| localStorage.setItem('active_chat_session_id', state.currentSessionId);
|
| ui.chatWindow.innerHTML = '';
|
| ui.chatTitle.textContent = "新規チャット";
|
| render.message({ sender: 'bot', text: 'こんにちは!新しい会話を始めましょう。' });
|
| await actions.updateSessions();
|
| },
|
| loadSession: async (sessionId) => {
|
| if (!sessionId) return;
|
| ui.chatTitle.textContent = "読み込み中...";
|
| const data = await api.getSession(sessionId);
|
| if (data && data.history) {
|
| ui.chatTitle.textContent = data.title || "新規チャット";
|
| render.chatHistory(data.history);
|
| } else { await actions.startNewChat(); }
|
| },
|
| updateSessions: async () => render.sessions(await api.getSessions()),
|
| handleFile: (file) => {
|
| if (file && file.type.startsWith('image/')) {
|
| state.attachedFile = file;
|
| render.filePreview();
|
| } else { alert('画像ファイル(PNG, JPGなど)のみ添付できます。'); }
|
| },
|
| setFormEnabled: (enabled) => {
|
| state.isLoading = !enabled;
|
| ui.messageInput.disabled = !enabled;
|
| ui.sendButton.disabled = !enabled;
|
| }
|
| };
|
|
|
|
|
| ui.menuButton.addEventListener('click', () => ui.sidebar.classList.toggle('visible'));
|
| ui.newChatButton.addEventListener('click', () => { actions.startNewChat(); ui.sidebar.classList.remove('visible'); });
|
| ui.toggleSettingsButton.addEventListener('click', () => ui.settingsPanel.classList.toggle('hidden'));
|
| ui.removeFileButton.addEventListener('click', () => { state.attachedFile = null; render.filePreview(); });
|
| ui.fileInput.addEventListener('change', () => ui.fileInput.files.length > 0 && actions.handleFile(ui.fileInput.files[0]));
|
| ui.savePersonalityButton.addEventListener('click', async () => {
|
| await api.setPersonality(ui.personalityInput.value);
|
| alert('人格を更新しました。');
|
| ui.settingsPanel.classList.add('hidden');
|
| });
|
|
|
| ui.sessionList.addEventListener('click', async (e) => {
|
| const item = e.target.closest('.session-item');
|
| if (!item) return;
|
| const sessionId = item.dataset.sessionId;
|
| const action = e.target.closest('.session-action-button');
|
| if (action) {
|
| e.stopPropagation();
|
| if (action.classList.contains('delete-button')) {
|
| if (confirm(`「${item.querySelector('.session-title').textContent}」を削除しますか?`)) {
|
| await api.deleteSession(sessionId);
|
| if (state.currentSessionId === sessionId) {
|
| state.currentSessionId = null; localStorage.removeItem('active_chat_session_id');
|
| }
|
| await actions.updateSessions();
|
| if (!state.currentSessionId) await actions.startNewChat();
|
| }
|
| } else if (action.classList.contains('rename-button')) {
|
| const newTitle = prompt("新しい名前:", item.querySelector('.session-title').textContent);
|
| if (newTitle && newTitle.trim()) await api.renameSession(sessionId, newTitle.trim());
|
| await actions.updateSessions();
|
| } else if (action.classList.contains('pin-button')) {
|
| await api.pinSession(sessionId); await actions.updateSessions();
|
| }
|
| } else {
|
| state.currentSessionId = sessionId; localStorage.setItem('active_chat_session_id', state.currentSessionId);
|
| await actions.loadSession(sessionId); render.activeSessionHighlight();
|
| ui.sidebar.classList.remove('visible');
|
| }
|
| });
|
|
|
| document.addEventListener('paste', e => {
|
| const file = Array.from(e.clipboardData.items).find(item => item.type.startsWith('image/'))?.getAsFile();
|
| if (file) { actions.handleFile(file); e.preventDefault(); }
|
| });
|
|
|
| ui.chatForm.addEventListener('submit', async (e) => {
|
| e.preventDefault();
|
| const userMessage = ui.messageInput.value.trim();
|
| if ((!userMessage && !state.attachedFile) || !state.currentSessionId || state.isLoading) return;
|
| actions.setFormEnabled(false);
|
| render.message({ sender: 'user', text: userMessage, imageFile: state.attachedFile });
|
| const formData = new FormData();
|
| formData.append('session_id', state.currentSessionId);
|
| formData.append('message', userMessage);
|
| if (state.attachedFile) formData.append('file', state.attachedFile);
|
| state.attachedFile = null; render.filePreview(); ui.messageInput.value = '';
|
| const loadingMessage = render.message({ sender: 'bot', text: '考え中...' });
|
| try {
|
| const data = await api.postChat(formData);
|
| ui.chatWindow.removeChild(loadingMessage);
|
| render.message({ sender: 'bot', text: data.reply || data.error });
|
| if (data.title) ui.chatTitle.textContent = data.title;
|
| await actions.updateSessions();
|
| } catch (error) {
|
| loadingMessage.textContent = "エラー: メッセージの送信に失敗しました。";
|
| } finally {
|
| actions.setFormEnabled(true);
|
| }
|
| });
|
|
|
|
|
| const initialize = async () => {
|
| await actions.updateSessions();
|
| const firstSession = ui.sessionList.querySelector('.session-item');
|
| let sessionToLoad = state.currentSessionId;
|
|
|
| if (sessionToLoad && !document.querySelector(`.session-item[data-session-id="${sessionToLoad}"]`)) {
|
| sessionToLoad = null;
|
| }
|
|
|
| if (!sessionToLoad && firstSession) {
|
| sessionToLoad = firstSession.dataset.sessionId;
|
| }
|
|
|
| if (sessionToLoad) {
|
| state.currentSessionId = sessionToLoad;
|
| localStorage.setItem('active_chat_session_id', state.currentSessionId);
|
| await actions.loadSession(state.currentSessionId);
|
| } else {
|
| await actions.startNewChat();
|
| }
|
| ui.sidebar.classList.remove('visible');
|
| };
|
|
|
| initialize();
|
| }); |