Mark-Lasfar
commited on
Commit
·
19bd628
1
Parent(s):
cf6fa01
add chat
Browse files- static/js/chat.js +110 -62
static/js/chat.js
CHANGED
|
@@ -155,6 +155,21 @@ function renderMarkdown(el) {
|
|
| 155 |
div.appendChild(t);
|
| 156 |
}
|
| 157 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
wrapper.querySelectorAll('hr').forEach(h => h.classList.add('styled-hr'));
|
| 159 |
Prism.highlightAllUnder(wrapper);
|
| 160 |
if (uiElements.chatBox) {
|
|
@@ -226,21 +241,31 @@ function addMsg(who, text) {
|
|
| 226 |
// إضافة أيقونات التحكم
|
| 227 |
const actions = document.createElement('div');
|
| 228 |
actions.className = 'message-actions';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
if (who === 'assistant') {
|
| 230 |
const retryBtn = document.createElement('button');
|
| 231 |
retryBtn.className = 'action-btn';
|
| 232 |
retryBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>';
|
| 233 |
retryBtn.title = 'Retry';
|
| 234 |
-
retryBtn.onclick = () => submitMessage();
|
| 235 |
actions.appendChild(retryBtn);
|
| 236 |
-
|
| 237 |
-
const copyBtn = document.createElement('button');
|
| 238 |
-
copyBtn.className = 'action-btn';
|
| 239 |
-
copyBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>';
|
| 240 |
-
copyBtn.title = 'Copy Response';
|
| 241 |
-
copyBtn.onclick = () => navigator.clipboard.writeText(text);
|
| 242 |
-
actions.appendChild(copyBtn);
|
| 243 |
}
|
|
|
|
|
|
|
| 244 |
if (who === 'user') {
|
| 245 |
const editBtn = document.createElement('button');
|
| 246 |
editBtn.className = 'action-btn';
|
|
@@ -751,7 +776,7 @@ async function submitMessage() {
|
|
| 751 |
return;
|
| 752 |
}
|
| 753 |
|
| 754 |
-
enterChatView();
|
| 755 |
|
| 756 |
if (uiElements.fileInput?.files.length > 0) {
|
| 757 |
const file = uiElements.fileInput.files[0];
|
|
@@ -793,57 +818,85 @@ async function submitMessage() {
|
|
| 793 |
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
|
| 794 |
}
|
| 795 |
streamMsg = addMsg('assistant', '');
|
| 796 |
-
const
|
| 797 |
-
|
| 798 |
-
|
|
|
|
| 799 |
updateUIForRequest();
|
| 800 |
|
| 801 |
isRequestActive = true;
|
| 802 |
abortController = new AbortController();
|
| 803 |
-
|
| 804 |
|
| 805 |
try {
|
| 806 |
const response = await sendRequest(endpoint, payload ? JSON.stringify(payload) : formData, headers);
|
| 807 |
let responseText = '';
|
| 808 |
-
if (endpoint === '/api/audio-transcription'
|
| 809 |
const data = await response.json();
|
| 810 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 811 |
} else {
|
| 812 |
-
const
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
break;
|
| 823 |
}
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 834 |
}
|
| 835 |
-
await new Promise(resolve => setTimeout(resolve, 20)); // تأخير لتأثير الكتابة
|
| 836 |
}
|
|
|
|
| 837 |
}
|
| 838 |
-
responseText = buffer;
|
| 839 |
}
|
| 840 |
|
|
|
|
|
|
|
|
|
|
| 841 |
if (streamMsg) {
|
| 842 |
streamMsg.dataset.text = responseText;
|
| 843 |
renderMarkdown(streamMsg);
|
| 844 |
streamMsg.dataset.done = '1';
|
| 845 |
}
|
| 846 |
-
attemptHistory.push({ role: 'assistant', content: responseText });
|
| 847 |
if (!(await checkAuth())) {
|
| 848 |
conversationHistory.push({ role: 'assistant', content: responseText });
|
| 849 |
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
|
|
@@ -851,25 +904,20 @@ async function submitMessage() {
|
|
| 851 |
finalizeRequest();
|
| 852 |
} catch (error) {
|
| 853 |
handleRequestError(error);
|
| 854 |
-
attemptHistory.push({ role: 'assistant', content: `Error: ${error.message || 'An error occurred'}` });
|
| 855 |
-
}
|
| 856 |
-
|
| 857 |
-
// تخزين تاريخ المحاولات
|
| 858 |
-
if (attemptHistory.length > 0) {
|
| 859 |
-
attemptHistory.forEach((attempt, index) => {
|
| 860 |
-
addAttemptHistory(attempt.role, attempt.content, index + 1);
|
| 861 |
-
});
|
| 862 |
}
|
| 863 |
}
|
| 864 |
|
| 865 |
let attemptCount = 0;
|
| 866 |
-
|
|
|
|
|
|
|
| 867 |
attemptCount++;
|
|
|
|
| 868 |
const container = document.createElement('div');
|
| 869 |
container.className = 'message-container';
|
| 870 |
const div = document.createElement('div');
|
| 871 |
div.className = `bubble ${who === 'user' ? 'bubble-user' : 'bubble-assist'} ${isArabicText(text) ? 'rtl' : ''}`;
|
| 872 |
-
div.dataset.text =
|
| 873 |
renderMarkdown(div);
|
| 874 |
|
| 875 |
const historyActions = document.createElement('div');
|
|
@@ -879,12 +927,10 @@ function addAttemptHistory(who, text, attemptNum) {
|
|
| 879 |
prevBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M15 19l-7-7 7-7"></path></svg>';
|
| 880 |
prevBtn.title = 'Previous Attempt';
|
| 881 |
prevBtn.onclick = () => {
|
| 882 |
-
if (
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
renderMarkdown(streamMsg);
|
| 887 |
-
}
|
| 888 |
}
|
| 889 |
};
|
| 890 |
const nextBtn = document.createElement('button');
|
|
@@ -892,16 +938,14 @@ function addAttemptHistory(who, text, attemptNum) {
|
|
| 892 |
nextBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 5l7 7-7 7"></path></svg>';
|
| 893 |
nextBtn.title = 'Next Attempt';
|
| 894 |
nextBtn.onclick = () => {
|
| 895 |
-
if (
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
renderMarkdown(streamMsg);
|
| 900 |
-
}
|
| 901 |
}
|
| 902 |
};
|
| 903 |
historyActions.appendChild(prevBtn);
|
| 904 |
-
historyActions.appendChild(document.createTextNode(`Attempt ${
|
| 905 |
historyActions.appendChild(nextBtn);
|
| 906 |
|
| 907 |
container.appendChild(div);
|
|
@@ -909,7 +953,11 @@ function addAttemptHistory(who, text, attemptNum) {
|
|
| 909 |
if (uiElements.chatBox) {
|
| 910 |
uiElements.chatBox.appendChild(container);
|
| 911 |
uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight;
|
|
|
|
|
|
|
|
|
|
| 912 |
}
|
|
|
|
| 913 |
}
|
| 914 |
|
| 915 |
// Stop streaming
|
|
|
|
| 155 |
div.appendChild(t);
|
| 156 |
}
|
| 157 |
});
|
| 158 |
+
// إضافة زر نسخ لكل بلوك كود
|
| 159 |
+
wrapper.querySelectorAll('pre').forEach(pre => {
|
| 160 |
+
const copyBtn = document.createElement('button');
|
| 161 |
+
copyBtn.className = 'copy-btn';
|
| 162 |
+
copyBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>';
|
| 163 |
+
copyBtn.title = 'Copy Code';
|
| 164 |
+
copyBtn.onclick = () => {
|
| 165 |
+
navigator.clipboard.writeText(pre.querySelector('code').innerText).then(() => {
|
| 166 |
+
copyBtn.textContent = 'Copied!';
|
| 167 |
+
setTimeout(() => { copyBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>'; }, 2000);
|
| 168 |
+
});
|
| 169 |
+
};
|
| 170 |
+
pre.appendChild(copyBtn);
|
| 171 |
+
});
|
| 172 |
+
|
| 173 |
wrapper.querySelectorAll('hr').forEach(h => h.classList.add('styled-hr'));
|
| 174 |
Prism.highlightAllUnder(wrapper);
|
| 175 |
if (uiElements.chatBox) {
|
|
|
|
| 241 |
// إضافة أيقونات التحكم
|
| 242 |
const actions = document.createElement('div');
|
| 243 |
actions.className = 'message-actions';
|
| 244 |
+
|
| 245 |
+
// زر نسخ الرد
|
| 246 |
+
const copyBtn = document.createElement('button');
|
| 247 |
+
copyBtn.className = 'action-btn';
|
| 248 |
+
copyBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>';
|
| 249 |
+
copyBtn.title = 'Copy Response';
|
| 250 |
+
copyBtn.onclick = () => {
|
| 251 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 252 |
+
copyBtn.textContent = 'Copied!';
|
| 253 |
+
setTimeout(() => { copyBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>'; }, 2000);
|
| 254 |
+
});
|
| 255 |
+
};
|
| 256 |
+
actions.appendChild(copyBtn);
|
| 257 |
+
|
| 258 |
+
// زر إعادة المحاولة (للمساعد فقط)
|
| 259 |
if (who === 'assistant') {
|
| 260 |
const retryBtn = document.createElement('button');
|
| 261 |
retryBtn.className = 'action-btn';
|
| 262 |
retryBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>';
|
| 263 |
retryBtn.title = 'Retry';
|
| 264 |
+
retryBtn.onclick = () => submitMessage(); // إعادة إرسال السؤال
|
| 265 |
actions.appendChild(retryBtn);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
}
|
| 267 |
+
|
| 268 |
+
// زر تعديل السؤال (للمستخدم فقط)
|
| 269 |
if (who === 'user') {
|
| 270 |
const editBtn = document.createElement('button');
|
| 271 |
editBtn.className = 'action-btn';
|
|
|
|
| 776 |
return;
|
| 777 |
}
|
| 778 |
|
| 779 |
+
enterChatView(); // دايمًا إظهار المحادثة قبل الإرسال
|
| 780 |
|
| 781 |
if (uiElements.fileInput?.files.length > 0) {
|
| 782 |
const file = uiElements.fileInput.files[0];
|
|
|
|
| 818 |
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
|
| 819 |
}
|
| 820 |
streamMsg = addMsg('assistant', '');
|
| 821 |
+
const thinkingEl = document.createElement('span');
|
| 822 |
+
thinkingEl.className = 'thinking';
|
| 823 |
+
thinkingEl.textContent = 'The model is thinking...';
|
| 824 |
+
streamMsg.appendChild(thinkingEl);
|
| 825 |
updateUIForRequest();
|
| 826 |
|
| 827 |
isRequestActive = true;
|
| 828 |
abortController = new AbortController();
|
| 829 |
+
const startTime = Date.now();
|
| 830 |
|
| 831 |
try {
|
| 832 |
const response = await sendRequest(endpoint, payload ? JSON.stringify(payload) : formData, headers);
|
| 833 |
let responseText = '';
|
| 834 |
+
if (endpoint === '/api/audio-transcription') {
|
| 835 |
const data = await response.json();
|
| 836 |
+
if (!data.transcription) throw new Error('No transcription received from server');
|
| 837 |
+
responseText = data.transcription || 'Error: No transcription generated.';
|
| 838 |
+
streamMsg.dataset.text = responseText;
|
| 839 |
+
renderMarkdown(streamMsg);
|
| 840 |
+
streamMsg.dataset.done = '1';
|
| 841 |
+
} else if (endpoint === '/api/image-analysis') {
|
| 842 |
+
const data = await response.json();
|
| 843 |
+
responseText = data.image_analysis || 'Error: No analysis generated.';
|
| 844 |
+
streamMsg.dataset.text = responseText;
|
| 845 |
+
renderMarkdown(streamMsg);
|
| 846 |
+
streamMsg.dataset.done = '1';
|
| 847 |
} else {
|
| 848 |
+
const contentType = response.headers.get('Content-Type');
|
| 849 |
+
if (contentType?.includes('application/json')) {
|
| 850 |
+
const data = await response.json();
|
| 851 |
+
responseText = data.response || 'Error: No response generated.';
|
| 852 |
+
if (data.conversation_id) {
|
| 853 |
+
currentConversationId = data.conversation_id;
|
| 854 |
+
currentConversationTitle = data.conversation_title || 'Untitled Conversation';
|
| 855 |
+
if (uiElements.conversationTitle) uiElements.conversationTitle.textContent = currentConversationTitle;
|
| 856 |
+
history.pushState(null, '', `/chat/${currentConversationId}`);
|
| 857 |
+
await loadConversations();
|
|
|
|
| 858 |
}
|
| 859 |
+
} else {
|
| 860 |
+
const reader = response.body.getReader();
|
| 861 |
+
const decoder = new TextDecoder();
|
| 862 |
+
let buffer = '';
|
| 863 |
+
streamMsg.dataset.text = '';
|
| 864 |
+
streamMsg.querySelector('.thinking')?.remove();
|
| 865 |
+
|
| 866 |
+
while (true) {
|
| 867 |
+
const { done, value } = await reader.read();
|
| 868 |
+
if (done) {
|
| 869 |
+
if (!buffer.trim()) throw new Error('Empty response from server');
|
| 870 |
+
break;
|
| 871 |
+
}
|
| 872 |
+
const chunk = decoder.decode(value, { stream: true });
|
| 873 |
+
buffer += chunk;
|
| 874 |
+
console.log('Received chunk:', chunk);
|
| 875 |
+
|
| 876 |
+
if (streamMsg) {
|
| 877 |
+
streamMsg.dataset.text = buffer;
|
| 878 |
+
currentAssistantText = buffer;
|
| 879 |
+
renderMarkdown(streamMsg);
|
| 880 |
+
streamMsg.style.opacity = '1';
|
| 881 |
+
if (uiElements.chatBox) {
|
| 882 |
+
uiElements.chatBox.style.display = 'flex';
|
| 883 |
+
uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight;
|
| 884 |
+
}
|
| 885 |
+
await new Promise(resolve => setTimeout(resolve, 30)); // تأخير 30ms لتأثير الكتابة الطبيعي
|
| 886 |
}
|
|
|
|
| 887 |
}
|
| 888 |
+
responseText = buffer;
|
| 889 |
}
|
|
|
|
| 890 |
}
|
| 891 |
|
| 892 |
+
const endTime = DateTime.now();
|
| 893 |
+
const thinkingTime = Math.round((endTime - startTime) / 1000); // حساب الوقت بالثواني
|
| 894 |
+
streamMsg.dataset.text += `\n\n*Processed in ${thinkingTime} seconds.*`;
|
| 895 |
if (streamMsg) {
|
| 896 |
streamMsg.dataset.text = responseText;
|
| 897 |
renderMarkdown(streamMsg);
|
| 898 |
streamMsg.dataset.done = '1';
|
| 899 |
}
|
|
|
|
| 900 |
if (!(await checkAuth())) {
|
| 901 |
conversationHistory.push({ role: 'assistant', content: responseText });
|
| 902 |
sessionStorage.setItem('conversationHistory', JSON.stringify(conversationHistory));
|
|
|
|
| 904 |
finalizeRequest();
|
| 905 |
} catch (error) {
|
| 906 |
handleRequestError(error);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 907 |
}
|
| 908 |
}
|
| 909 |
|
| 910 |
let attemptCount = 0;
|
| 911 |
+
let attempts = []; // لتخزين الردود السابقة
|
| 912 |
+
|
| 913 |
+
function addAttemptHistory(who, text) {
|
| 914 |
attemptCount++;
|
| 915 |
+
attempts.push(text); // تخزين الرد
|
| 916 |
const container = document.createElement('div');
|
| 917 |
container.className = 'message-container';
|
| 918 |
const div = document.createElement('div');
|
| 919 |
div.className = `bubble ${who === 'user' ? 'bubble-user' : 'bubble-assist'} ${isArabicText(text) ? 'rtl' : ''}`;
|
| 920 |
+
div.dataset.text = '';
|
| 921 |
renderMarkdown(div);
|
| 922 |
|
| 923 |
const historyActions = document.createElement('div');
|
|
|
|
| 927 |
prevBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M15 19l-7-7 7-7"></path></svg>';
|
| 928 |
prevBtn.title = 'Previous Attempt';
|
| 929 |
prevBtn.onclick = () => {
|
| 930 |
+
if (attemptCount > 1) {
|
| 931 |
+
attemptCount--;
|
| 932 |
+
div.dataset.text = attempts[attemptCount - 1];
|
| 933 |
+
renderMarkdown(div);
|
|
|
|
|
|
|
| 934 |
}
|
| 935 |
};
|
| 936 |
const nextBtn = document.createElement('button');
|
|
|
|
| 938 |
nextBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 5l7 7-7 7"></path></svg>';
|
| 939 |
nextBtn.title = 'Next Attempt';
|
| 940 |
nextBtn.onclick = () => {
|
| 941 |
+
if (attemptCount < attempts.length) {
|
| 942 |
+
attemptCount++;
|
| 943 |
+
div.dataset.text = attempts[attemptCount - 1];
|
| 944 |
+
renderMarkdown(div);
|
|
|
|
|
|
|
| 945 |
}
|
| 946 |
};
|
| 947 |
historyActions.appendChild(prevBtn);
|
| 948 |
+
historyActions.appendChild(document.createTextNode(`Attempt ${attemptCount}`));
|
| 949 |
historyActions.appendChild(nextBtn);
|
| 950 |
|
| 951 |
container.appendChild(div);
|
|
|
|
| 953 |
if (uiElements.chatBox) {
|
| 954 |
uiElements.chatBox.appendChild(container);
|
| 955 |
uiElements.chatBox.scrollTop = uiElements.chatBox.scrollHeight;
|
| 956 |
+
} else {
|
| 957 |
+
console.error('chatBox not found, appending to a fallback container');
|
| 958 |
+
document.body.appendChild(container);
|
| 959 |
}
|
| 960 |
+
return div;
|
| 961 |
}
|
| 962 |
|
| 963 |
// Stop streaming
|