|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
}); |