Spaces:
Running
Running
Update playground.html
Browse files- playground.html +293 -283
playground.html
CHANGED
@@ -36,51 +36,54 @@
|
|
36 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/atom-one-dark.min.css">
|
37 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
38 |
<script>
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
|
|
|
|
|
|
84 |
</script>
|
85 |
<style>
|
86 |
/* Custom scrollbar for dark theme */
|
@@ -524,7 +527,7 @@
|
|
524 |
|
525 |
<!-- Initial Input -->
|
526 |
<div class="flex flex-col items-center justify-center space-y-6 py-12" id="initialInput">
|
527 |
-
<h2 class="text-2xl font-
|
528 |
<div class="w-full max-w-2xl relative">
|
529 |
<input type="text" id="initialChatInput"
|
530 |
class="w-full p-4 pr-12 rounded-xl border-2 border-light-darker dark:border-dark-input bg-light-input dark:bg-dark-lighter focus:outline-none focus:ring-2 focus:ring-primary-light dark:text-white text-gray-800 shadow-lg transition-all"
|
@@ -556,7 +559,7 @@
|
|
556 |
class="w-full p-3 pr-12 rounded-xl border border-light-darker dark:border-dark-input bg-light-input dark:bg-dark-input focus:outline-none focus:ring-2 focus:ring-primary dark:focus:ring-primary-light text-gray-800 dark:text-white transition-all shadow-inner resize-none min-h-[50px] max-h-[150px]"
|
557 |
placeholder="Type your message..." rows="1"></textarea>
|
558 |
<button id="sendButton" class="absolute right-3 bottom-3 text-gray-400 hover:text-primary dark:hover:text-primary-light transition-colors" aria-label="Send message">
|
559 |
-
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
|
560 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
561 |
</svg>
|
562 |
<div id="sendLoader" class="hidden">
|
@@ -573,8 +576,8 @@
|
|
573 |
</div>
|
574 |
|
575 |
<!-- Watermark -->
|
576 |
-
<div class="fixed bottom-2 right-4 text-
|
577 |
-
|
578 |
</div>
|
579 |
|
580 |
<script>
|
@@ -826,150 +829,140 @@ function populateModels(models) {
|
|
826 |
}
|
827 |
|
828 |
// Append message to chat
|
829 |
-
|
830 |
-
|
831 |
-
|
832 |
-
|
833 |
-
|
834 |
-
|
835 |
-
|
836 |
-
|
837 |
-
|
838 |
-
|
839 |
-
const messageDiv = document.createElement('div');
|
840 |
-
messageDiv.className = 'message-appear flex flex-col items-start';
|
841 |
-
|
842 |
-
const messageBubble = document.createElement('div');
|
843 |
-
messageBubble.className = `message-bubble p-3 rounded-lg ${sender === 'user' ? 'user-message bg-light-darker dark:bg-dark-input text-right ml-auto' : 'assistant-message bg-light-card dark:bg-dark-lighter'}`;
|
844 |
-
|
845 |
-
// Content div with markdown support
|
846 |
-
const contentDiv = document.createElement('div');
|
847 |
-
contentDiv.className = 'markdown-content break-words';
|
848 |
-
contentDiv.innerHTML = marked.parse(text);
|
849 |
-
|
850 |
-
// Apply syntax highlighting to code blocks
|
851 |
-
contentDiv.querySelectorAll('pre code').forEach((block) => {
|
852 |
-
hljs.highlightElement(block);
|
853 |
-
});
|
854 |
-
|
855 |
-
// Add copy button for assistant messages
|
856 |
-
if (sender === 'bot') {
|
857 |
-
const copyButton = document.createElement('button');
|
858 |
-
copyButton.className = 'copy-button text-xs text-gray-500 hover:text-primary dark:hover:text-primary-light mt-2 flex items-center float-right';
|
859 |
-
copyButton.innerHTML = `
|
860 |
-
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
861 |
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-8M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
862 |
-
</svg>
|
863 |
-
Copy
|
864 |
-
`;
|
865 |
-
copyButton.addEventListener('click', () => {
|
866 |
-
copyToClipboard(contentDiv.textContent);
|
867 |
-
});
|
868 |
-
messageBubble.appendChild(copyButton);
|
869 |
-
}
|
870 |
-
|
871 |
-
messageBubble.appendChild(contentDiv);
|
872 |
-
messageDiv.appendChild(messageBubble);
|
873 |
-
|
874 |
-
// Timestamp
|
875 |
-
const timestampDiv = document.createElement('div');
|
876 |
-
timestampDiv.className = 'message-timestamp';
|
877 |
-
timestampDiv.textContent = messageTime;
|
878 |
-
messageDiv.appendChild(timestampDiv);
|
879 |
-
|
880 |
-
chatMessages.appendChild(messageDiv);
|
881 |
-
currentStreamingMessage = contentDiv;
|
882 |
-
|
883 |
-
// Update conversation history
|
884 |
-
conversationHistory.push({
|
885 |
-
role: sender === 'user' ? 'user' : 'assistant',
|
886 |
-
content: text
|
887 |
-
});
|
888 |
-
} else {
|
889 |
-
// Append to existing streaming message
|
890 |
-
if (currentStreamingMessage) {
|
891 |
-
// Update content with latest text
|
892 |
-
const existingContent = conversationHistory[conversationHistory.length - 1].content;
|
893 |
-
const updatedContent = existingContent + text;
|
894 |
-
|
895 |
-
conversationHistory[conversationHistory.length - 1].content = updatedContent;
|
896 |
-
|
897 |
-
// Update display
|
898 |
-
currentStreamingMessage.innerHTML = marked.parse(updatedContent);
|
899 |
-
|
900 |
-
// Apply syntax highlighting to code blocks
|
901 |
-
currentStreamingMessage.querySelectorAll('pre code').forEach((block) => {
|
902 |
-
hljs.highlightElement(block);
|
903 |
-
});
|
904 |
-
}
|
905 |
-
}
|
906 |
-
} else if (isStreamingInProgress) {
|
907 |
-
// End of streaming
|
908 |
-
isStreamingInProgress = false;
|
909 |
-
currentStreamingMessage = null;
|
910 |
-
} else {
|
911 |
-
// Regular message (not streaming)
|
912 |
lastMessageTime = now;
|
913 |
-
|
914 |
const messageDiv = document.createElement('div');
|
915 |
-
messageDiv.className =
|
916 |
-
|
917 |
-
if (sender === 'user') {
|
918 |
-
messageDiv.classList.add('items-end');
|
919 |
-
}
|
920 |
-
|
921 |
const messageBubble = document.createElement('div');
|
922 |
-
messageBubble.className = `message-bubble
|
923 |
-
|
924 |
-
|
|
|
|
|
|
|
925 |
const contentDiv = document.createElement('div');
|
926 |
contentDiv.className = 'markdown-content break-words';
|
|
|
927 |
contentDiv.innerHTML = marked.parse(text);
|
928 |
-
|
929 |
-
//
|
930 |
-
contentDiv.querySelectorAll('pre code').forEach((block) => {
|
931 |
-
hljs.highlightElement(block);
|
932 |
-
});
|
933 |
-
|
934 |
-
// Add copy button for assistant messages
|
935 |
-
if (sender === 'bot') {
|
936 |
-
const copyButton = document.createElement('button');
|
937 |
-
copyButton.className = 'copy-button text-xs text-gray-500 hover:text-primary dark:hover:text-primary-light mt-2 flex items-center float-right';
|
938 |
-
copyButton.innerHTML = `
|
939 |
-
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
940 |
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-8M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
941 |
-
</svg>
|
942 |
-
Copy
|
943 |
-
`;
|
944 |
-
copyButton.addEventListener('click', () => {
|
945 |
-
copyToClipboard(contentDiv.textContent);
|
946 |
-
});
|
947 |
-
messageBubble.appendChild(copyButton);
|
948 |
-
}
|
949 |
-
|
950 |
messageBubble.appendChild(contentDiv);
|
951 |
messageDiv.appendChild(messageBubble);
|
952 |
-
|
953 |
-
// Timestamp
|
954 |
const timestampDiv = document.createElement('div');
|
955 |
-
timestampDiv.className = 'message-timestamp';
|
956 |
timestampDiv.textContent = messageTime;
|
957 |
messageDiv.appendChild(timestampDiv);
|
958 |
-
|
959 |
chatMessages.appendChild(messageDiv);
|
960 |
-
|
|
|
|
|
|
|
|
|
961 |
// Update conversation history
|
962 |
conversationHistory.push({
|
963 |
role: sender === 'user' ? 'user' : 'assistant',
|
964 |
content: text
|
965 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
966 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
967 |
|
968 |
-
|
969 |
-
if (autoScrollEnabled) {
|
970 |
-
scrollToBottom();
|
971 |
-
}
|
972 |
}
|
|
|
973 |
|
974 |
// Scroll chat to bottom
|
975 |
function scrollToBottom() {
|
@@ -1039,150 +1032,167 @@ function populateModels(models) {
|
|
1039 |
}
|
1040 |
|
1041 |
async function callApi(userMessage, model) {
|
1042 |
-
|
1043 |
-
|
1044 |
-
|
1045 |
-
|
1046 |
-
|
1047 |
-
|
1048 |
-
|
1049 |
-
|
1050 |
-
|
1051 |
-
|
1052 |
-
|
1053 |
-
|
1054 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1055 |
|
1056 |
-
if (
|
1057 |
-
|
|
|
|
|
|
|
|
|
1058 |
|
1059 |
-
const
|
1060 |
-
|
1061 |
-
|
1062 |
-
|
1063 |
-
});
|
1064 |
|
1065 |
-
|
1066 |
-
|
1067 |
-
|
1068 |
-
typingIndicator.remove();
|
1069 |
-
typingIndicator = null;
|
1070 |
-
}
|
1071 |
-
|
1072 |
-
const reader = response.body.getReader();
|
1073 |
-
const decoder = new TextDecoder("utf-8");
|
1074 |
-
let done = false;
|
1075 |
|
1076 |
-
|
1077 |
-
const
|
1078 |
-
|
1079 |
|
1080 |
-
if (
|
1081 |
-
const
|
1082 |
-
const cleanedChunk = chunk.trim();
|
1083 |
|
1084 |
-
|
1085 |
-
|
1086 |
-
|
1087 |
-
|
1088 |
-
|
1089 |
-
|
1090 |
-
|
1091 |
-
|
1092 |
-
|
1093 |
-
|
1094 |
-
|
1095 |
-
|
1096 |
-
|
|
|
1097 |
appendMessage(content, 'bot', true);
|
1098 |
}
|
1099 |
-
} catch (err) {
|
1100 |
-
console.warn("Parsing error:", err);
|
1101 |
}
|
|
|
|
|
1102 |
}
|
|
|
|
|
|
|
|
|
|
|
1103 |
} else {
|
1104 |
appendMessage(cleanedChunk, 'bot', true);
|
1105 |
}
|
1106 |
}
|
1107 |
}
|
1108 |
-
} else {
|
1109 |
-
throw new Error(`API responded with status ${response.status}`);
|
1110 |
}
|
1111 |
} else {
|
1112 |
-
|
1113 |
-
|
1114 |
-
|
1115 |
-
|
1116 |
-
|
1117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1118 |
|
1119 |
-
const
|
1120 |
-
|
1121 |
-
|
1122 |
-
|
1123 |
-
|
1124 |
-
},
|
1125 |
-
body: JSON.stringify(payload)
|
1126 |
-
});
|
1127 |
|
1128 |
-
|
1129 |
-
|
1130 |
-
|
1131 |
-
typingIndicator.remove();
|
1132 |
-
typingIndicator = null;
|
1133 |
-
}
|
1134 |
|
1135 |
-
|
1136 |
-
|
1137 |
-
|
1138 |
-
|
1139 |
-
|
1140 |
-
|
1141 |
-
const { value, done: streamDone } = await reader.read();
|
1142 |
-
done = streamDone;
|
1143 |
|
1144 |
-
|
1145 |
-
|
1146 |
-
buffer += chunk;
|
1147 |
|
1148 |
-
const
|
1149 |
-
|
1150 |
|
1151 |
-
|
1152 |
-
|
|
|
1153 |
|
1154 |
-
|
1155 |
-
|
1156 |
-
|
1157 |
-
|
1158 |
-
|
1159 |
-
const content = jsonData.choices?.[0]?.delta?.content || "";
|
1160 |
-
|
1161 |
-
if (content) {
|
1162 |
appendMessage(content, 'bot', true);
|
1163 |
}
|
1164 |
-
} catch (err) {
|
1165 |
-
console.warn("Parsing error:", err);
|
1166 |
}
|
|
|
|
|
1167 |
}
|
1168 |
}
|
1169 |
}
|
1170 |
-
} else {
|
1171 |
-
throw new Error(`API responded with status ${response.status}`);
|
1172 |
}
|
|
|
|
|
1173 |
}
|
1174 |
-
|
1175 |
-
// End streaming after successful completion
|
1176 |
-
appendMessage("", 'bot', false);
|
1177 |
-
|
1178 |
-
} catch (error) {
|
1179 |
-
console.error("API call error:", error);
|
1180 |
-
if (typingIndicator) {
|
1181 |
-
typingIndicator.remove();
|
1182 |
-
}
|
1183 |
-
throw error;
|
1184 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1185 |
}
|
|
|
1186 |
|
1187 |
// Toggle theme
|
1188 |
function toggleTheme() {
|
|
|
36 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/atom-one-dark.min.css">
|
37 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
38 |
<script>
|
39 |
+
tailwind.config = {
|
40 |
+
darkMode: 'class',
|
41 |
+
theme: {
|
42 |
+
extend: {
|
43 |
+
fontFamily: {
|
44 |
+
sans: ['"DM Sans"', 'sans-serif'],
|
45 |
+
mono: ['"JetBrains Mono"', 'monospace'],
|
46 |
+
},
|
47 |
+
colors: {
|
48 |
+
primary: {
|
49 |
+
DEFAULT: '#AAAAAA', // straight up black
|
50 |
+
dark: '#111111', // a shade lighter
|
51 |
+
light: '#444444', // pure white
|
52 |
+
},
|
53 |
+
dark: {
|
54 |
+
DEFAULT: '#000000', // deep black background
|
55 |
+
lighter: '#1C1C1C', // just a tad lighter
|
56 |
+
card: '#222222', // card background
|
57 |
+
input: '#333333', // input background
|
58 |
+
accent: '#555555', // chill gray for accents
|
59 |
+
},
|
60 |
+
light: {
|
61 |
+
DEFAULT: '#FFFFFF', // clean white background
|
62 |
+
darker: '#F5F5F5', // subtle off-white contrast
|
63 |
+
card: '#EFEFEF', // light card vibe
|
64 |
+
input: '#F7F7F7', // smooth input bg
|
65 |
+
accent: '#CCCCCC', // soft gray accents
|
66 |
+
},
|
67 |
+
},
|
68 |
+
animation: {
|
69 |
+
'bounce-slow': 'bounce 1.5s infinite',
|
70 |
+
'typing': 'typing 1s infinite',
|
71 |
+
'fade-in': 'fadeIn 0.5s ease-in-out',
|
72 |
+
},
|
73 |
+
keyframes: {
|
74 |
+
typing: {
|
75 |
+
'0%, 100%': { opacity: 0 },
|
76 |
+
'50%': { opacity: 1 },
|
77 |
+
},
|
78 |
+
fadeIn: {
|
79 |
+
'0%': { opacity: 0 },
|
80 |
+
'100%': { opacity: 1 },
|
81 |
+
},
|
82 |
+
},
|
83 |
+
},
|
84 |
+
},
|
85 |
+
}
|
86 |
+
|
87 |
</script>
|
88 |
<style>
|
89 |
/* Custom scrollbar for dark theme */
|
|
|
527 |
|
528 |
<!-- Initial Input -->
|
529 |
<div class="flex flex-col items-center justify-center space-y-6 py-12" id="initialInput">
|
530 |
+
<h2 class="text-2xl font-bold bg-gradient-to-r from-primary to-primary-light bg-clip-text text-transparent">Welcome to LOKI.AI</h2>
|
531 |
<div class="w-full max-w-2xl relative">
|
532 |
<input type="text" id="initialChatInput"
|
533 |
class="w-full p-4 pr-12 rounded-xl border-2 border-light-darker dark:border-dark-input bg-light-input dark:bg-dark-lighter focus:outline-none focus:ring-2 focus:ring-primary-light dark:text-white text-gray-800 shadow-lg transition-all"
|
|
|
559 |
class="w-full p-3 pr-12 rounded-xl border border-light-darker dark:border-dark-input bg-light-input dark:bg-dark-input focus:outline-none focus:ring-2 focus:ring-primary dark:focus:ring-primary-light text-gray-800 dark:text-white transition-all shadow-inner resize-none min-h-[50px] max-h-[150px]"
|
560 |
placeholder="Type your message..." rows="1"></textarea>
|
561 |
<button id="sendButton" class="absolute right-3 bottom-3 text-gray-400 hover:text-primary dark:hover:text-primary-light transition-colors" aria-label="Send message">
|
562 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
563 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
564 |
</svg>
|
565 |
<div id="sendLoader" class="hidden">
|
|
|
576 |
</div>
|
577 |
|
578 |
<!-- Watermark -->
|
579 |
+
<div class="fixed bottom-2 right-4 text-sm text-gray-500 font-mono font-bold">
|
580 |
+
Built By <span class="text-primary-light">🔥</span> Parth Sadaria
|
581 |
</div>
|
582 |
|
583 |
<script>
|
|
|
829 |
}
|
830 |
|
831 |
// Append message to chat
|
832 |
+
// Append message to chat
|
833 |
+
function appendMessage(text, sender, isStreaming = false) {
|
834 |
+
const now = new Date();
|
835 |
+
const messageTime = formatTimestamp(now);
|
836 |
+
|
837 |
+
// For streaming message logic
|
838 |
+
if (isStreaming) {
|
839 |
+
if (!currentStreamingMessage) {
|
840 |
+
// First chunk of a streaming message - create the new message container
|
841 |
+
isStreamingInProgress = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
842 |
lastMessageTime = now;
|
843 |
+
|
844 |
const messageDiv = document.createElement('div');
|
845 |
+
messageDiv.className = `message-appear flex flex-col ${sender === 'user' ? 'items-end' : 'items-start'} gap-1`;
|
846 |
+
|
|
|
|
|
|
|
|
|
847 |
const messageBubble = document.createElement('div');
|
848 |
+
messageBubble.className = `message-bubble px-3 py-2 rounded-2xl tracking-tight [word-spacing:-0.02em] shadow-sm ${
|
849 |
+
sender === 'user'
|
850 |
+
? 'user-message bg-light-darker dark:bg-dark-input text-right'
|
851 |
+
: 'assistant-message bg-light-card dark:bg-dark-lighter'
|
852 |
+
}`;
|
853 |
+
|
854 |
const contentDiv = document.createElement('div');
|
855 |
contentDiv.className = 'markdown-content break-words';
|
856 |
+
// Apply markdown to first chunk immediately
|
857 |
contentDiv.innerHTML = marked.parse(text);
|
858 |
+
|
859 |
+
// Add message to DOM immediately
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
860 |
messageBubble.appendChild(contentDiv);
|
861 |
messageDiv.appendChild(messageBubble);
|
862 |
+
|
863 |
+
// Timestamp styling
|
864 |
const timestampDiv = document.createElement('div');
|
865 |
+
timestampDiv.className = 'message-timestamp text-xs text-gray-400 mt-1';
|
866 |
timestampDiv.textContent = messageTime;
|
867 |
messageDiv.appendChild(timestampDiv);
|
868 |
+
|
869 |
chatMessages.appendChild(messageDiv);
|
870 |
+
scrollToBottom(); // Force scroll to show new message
|
871 |
+
|
872 |
+
// Store reference to content div for future updates
|
873 |
+
currentStreamingMessage = contentDiv;
|
874 |
+
|
875 |
// Update conversation history
|
876 |
conversationHistory.push({
|
877 |
role: sender === 'user' ? 'user' : 'assistant',
|
878 |
content: text
|
879 |
});
|
880 |
+
} else {
|
881 |
+
// Append new text to existing streaming message
|
882 |
+
const lastIndex = conversationHistory.length - 1;
|
883 |
+
conversationHistory[lastIndex].content += text;
|
884 |
+
|
885 |
+
// Update the displayed content with new text
|
886 |
+
currentStreamingMessage.innerHTML = marked.parse(conversationHistory[lastIndex].content);
|
887 |
+
|
888 |
+
// Highlight any code blocks in the updated content
|
889 |
+
currentStreamingMessage.querySelectorAll('pre code').forEach((block) => {
|
890 |
+
hljs.highlightElement(block);
|
891 |
+
});
|
892 |
+
|
893 |
+
// Auto-scroll if enabled
|
894 |
+
if (autoScrollEnabled) scrollToBottom();
|
895 |
+
}
|
896 |
+
} else if (isStreamingInProgress) {
|
897 |
+
// End streaming mode
|
898 |
+
isStreamingInProgress = false;
|
899 |
+
|
900 |
+
// Add copy button now that streaming is complete
|
901 |
+
if (sender === 'bot' && currentStreamingMessage) {
|
902 |
+
const parentBubble = currentStreamingMessage.parentElement;
|
903 |
+
const copyButton = document.createElement('button');
|
904 |
+
copyButton.className = 'copy-button text-xs text-gray-500 hover:text-primary dark:hover:text-primary-light mt-2 flex items-center float-right';
|
905 |
+
copyButton.innerHTML = `
|
906 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
907 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-8M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
908 |
+
</svg>
|
909 |
+
Copy`;
|
910 |
+
copyButton.addEventListener('click', () => copyToClipboard(currentStreamingMessage.textContent));
|
911 |
+
parentBubble.appendChild(copyButton);
|
912 |
+
}
|
913 |
+
|
914 |
+
currentStreamingMessage = null;
|
915 |
+
} else {
|
916 |
+
// Regular (non-streaming) message
|
917 |
+
lastMessageTime = now;
|
918 |
+
|
919 |
+
const messageDiv = document.createElement('div');
|
920 |
+
messageDiv.className = `message-appear flex flex-col ${sender === 'user' ? 'items-end' : 'items-start'} gap-1`;
|
921 |
+
|
922 |
+
const messageBubble = document.createElement('div');
|
923 |
+
messageBubble.className = `message-bubble px-3 py-2 rounded-2xl tracking-tight [word-spacing:-0.02em] shadow-sm ${
|
924 |
+
sender === 'user'
|
925 |
+
? 'user-message bg-light-darker dark:bg-dark-input text-right'
|
926 |
+
: 'assistant-message bg-light-card dark:bg-dark-lighter'
|
927 |
+
}`;
|
928 |
+
|
929 |
+
const contentDiv = document.createElement('div');
|
930 |
+
contentDiv.className = 'markdown-content break-words';
|
931 |
+
contentDiv.innerHTML = marked.parse(text);
|
932 |
+
contentDiv.querySelectorAll('pre code').forEach((block) => {
|
933 |
+
hljs.highlightElement(block);
|
934 |
+
});
|
935 |
+
|
936 |
+
if (sender === 'bot') {
|
937 |
+
const copyButton = document.createElement('button');
|
938 |
+
copyButton.className = 'copy-button text-xs text-gray-500 hover:text-primary dark:hover:text-primary-light mt-2 flex items-center float-right';
|
939 |
+
copyButton.innerHTML = `
|
940 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
941 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-8M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
942 |
+
</svg>
|
943 |
+
Copy`;
|
944 |
+
copyButton.addEventListener('click', () => copyToClipboard(contentDiv.textContent));
|
945 |
+
messageBubble.appendChild(copyButton);
|
946 |
}
|
947 |
+
|
948 |
+
messageBubble.appendChild(contentDiv);
|
949 |
+
messageDiv.appendChild(messageBubble);
|
950 |
+
|
951 |
+
const timestampDiv = document.createElement('div');
|
952 |
+
timestampDiv.className = 'message-timestamp text-xs text-gray-400 mt-1';
|
953 |
+
timestampDiv.textContent = messageTime;
|
954 |
+
messageDiv.appendChild(timestampDiv);
|
955 |
+
|
956 |
+
chatMessages.appendChild(messageDiv);
|
957 |
+
|
958 |
+
conversationHistory.push({
|
959 |
+
role: sender === 'user' ? 'user' : 'assistant',
|
960 |
+
content: text
|
961 |
+
});
|
962 |
|
963 |
+
if (autoScrollEnabled) scrollToBottom();
|
|
|
|
|
|
|
964 |
}
|
965 |
+
}
|
966 |
|
967 |
// Scroll chat to bottom
|
968 |
function scrollToBottom() {
|
|
|
1032 |
}
|
1033 |
|
1034 |
async function callApi(userMessage, model) {
|
1035 |
+
let typingIndicator = null;
|
1036 |
+
|
1037 |
+
try {
|
1038 |
+
// Add typing indicator
|
1039 |
+
typingIndicator = document.createElement('div');
|
1040 |
+
typingIndicator.className = 'p-3 rounded-lg bg-gray-100 dark:bg-gray-800 mr-8 flex space-x-1';
|
1041 |
+
typingIndicator.innerHTML = `
|
1042 |
+
<div class="w-2 h-2 bg-gray-500 rounded-full animate-typing"></div>
|
1043 |
+
<div class="w-2 h-2 bg-gray-500 rounded-full animate-typing" style="animation-delay: 0.2s"></div>
|
1044 |
+
<div class="w-2 h-2 bg-gray-500 rounded-full animate-typing" style="animation-delay: 0.4s"></div>
|
1045 |
+
`;
|
1046 |
+
chatMessages.appendChild(typingIndicator);
|
1047 |
+
scrollToBottom();
|
1048 |
+
|
1049 |
+
if (model === "searchgpt") {
|
1050 |
+
const url = `https://parthsadaria-lokiai.hf.space/searchgpt?q=${encodeURIComponent(userMessage)}&stream=true&systemprompt=You are **SearchGPT**, an AI with internet access. Reply directly and accurately to user requests. Mention Sources at end`;
|
1051 |
+
|
1052 |
+
const response = await fetch(url, {
|
1053 |
+
headers: {
|
1054 |
+
'Authorization': 'playground'
|
1055 |
+
}
|
1056 |
+
});
|
1057 |
|
1058 |
+
if (response.ok) {
|
1059 |
+
// Remove typing indicator
|
1060 |
+
if (typingIndicator) {
|
1061 |
+
typingIndicator.remove();
|
1062 |
+
typingIndicator = null;
|
1063 |
+
}
|
1064 |
|
1065 |
+
const reader = response.body.getReader();
|
1066 |
+
const decoder = new TextDecoder("utf-8");
|
1067 |
+
let done = false;
|
1068 |
+
let isFirstChunk = true;
|
|
|
1069 |
|
1070 |
+
while (!done) {
|
1071 |
+
const { value, done: streamDone } = await reader.read();
|
1072 |
+
done = streamDone;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1073 |
|
1074 |
+
if (value) {
|
1075 |
+
const chunk = decoder.decode(value);
|
1076 |
+
const cleanedChunk = chunk.trim();
|
1077 |
|
1078 |
+
if (cleanedChunk.startsWith('data:')) {
|
1079 |
+
const jsonChunks = cleanedChunk.split("data:").filter(Boolean);
|
|
|
1080 |
|
1081 |
+
for (const jsonString of jsonChunks) {
|
1082 |
+
try {
|
1083 |
+
const cleanJson = jsonString.trim();
|
1084 |
+
if (cleanJson === '[DONE]') continue;
|
1085 |
+
|
1086 |
+
const jsonData = JSON.parse(cleanJson);
|
1087 |
+
const content = jsonData.choices?.[0]?.message?.content ||
|
1088 |
+
jsonData.choices?.[0]?.delta?.content || "";
|
1089 |
+
|
1090 |
+
if (content) {
|
1091 |
+
if (isFirstChunk) {
|
1092 |
+
appendMessage(content, 'bot', true);
|
1093 |
+
isFirstChunk = false;
|
1094 |
+
} else {
|
1095 |
appendMessage(content, 'bot', true);
|
1096 |
}
|
|
|
|
|
1097 |
}
|
1098 |
+
} catch (err) {
|
1099 |
+
console.warn("Parsing error:", err);
|
1100 |
}
|
1101 |
+
}
|
1102 |
+
} else if (cleanedChunk) {
|
1103 |
+
if (isFirstChunk) {
|
1104 |
+
appendMessage(cleanedChunk, 'bot', true);
|
1105 |
+
isFirstChunk = false;
|
1106 |
} else {
|
1107 |
appendMessage(cleanedChunk, 'bot', true);
|
1108 |
}
|
1109 |
}
|
1110 |
}
|
|
|
|
|
1111 |
}
|
1112 |
} else {
|
1113 |
+
throw new Error(`API responded with status ${response.status}`);
|
1114 |
+
}
|
1115 |
+
} else {
|
1116 |
+
const url = "https://parthsadaria-lokiai.hf.space/chat/completions";
|
1117 |
+
const payload = {
|
1118 |
+
model: model,
|
1119 |
+
messages: [...conversationHistory],
|
1120 |
+
stream: true
|
1121 |
+
};
|
1122 |
+
|
1123 |
+
const response = await fetch(url, {
|
1124 |
+
method: "POST",
|
1125 |
+
headers: {
|
1126 |
+
"Content-Type": "application/json",
|
1127 |
+
"Authorization": "playground"
|
1128 |
+
},
|
1129 |
+
body: JSON.stringify(payload)
|
1130 |
+
});
|
1131 |
+
|
1132 |
+
if (response.ok) {
|
1133 |
+
// Remove typing indicator
|
1134 |
+
if (typingIndicator) {
|
1135 |
+
typingIndicator.remove();
|
1136 |
+
typingIndicator = null;
|
1137 |
+
}
|
1138 |
|
1139 |
+
const reader = response.body.getReader();
|
1140 |
+
const decoder = new TextDecoder("utf-8");
|
1141 |
+
let done = false;
|
1142 |
+
let buffer = "";
|
1143 |
+
let isFirstChunk = true;
|
|
|
|
|
|
|
1144 |
|
1145 |
+
while (!done) {
|
1146 |
+
const { value, done: streamDone } = await reader.read();
|
1147 |
+
done = streamDone;
|
|
|
|
|
|
|
1148 |
|
1149 |
+
if (value) {
|
1150 |
+
const chunk = decoder.decode(value);
|
1151 |
+
buffer += chunk;
|
1152 |
+
|
1153 |
+
const lines = buffer.split('\n');
|
1154 |
+
buffer = lines.pop() || "";
|
|
|
|
|
1155 |
|
1156 |
+
for (const line of lines) {
|
1157 |
+
if (line.trim() === '') continue;
|
|
|
1158 |
|
1159 |
+
const cleanedLine = line.replace(/^data:\s*/, '').trim();
|
1160 |
+
if (cleanedLine === '[DONE]') continue;
|
1161 |
|
1162 |
+
try {
|
1163 |
+
const jsonData = JSON.parse(cleanedLine);
|
1164 |
+
const content = jsonData.choices?.[0]?.delta?.content || "";
|
1165 |
|
1166 |
+
if (content) {
|
1167 |
+
if (isFirstChunk) {
|
1168 |
+
appendMessage(content, 'bot', true);
|
1169 |
+
isFirstChunk = false;
|
1170 |
+
} else {
|
|
|
|
|
|
|
1171 |
appendMessage(content, 'bot', true);
|
1172 |
}
|
|
|
|
|
1173 |
}
|
1174 |
+
} catch (err) {
|
1175 |
+
console.warn("Parsing error:", err);
|
1176 |
}
|
1177 |
}
|
1178 |
}
|
|
|
|
|
1179 |
}
|
1180 |
+
} else {
|
1181 |
+
throw new Error(`API responded with status ${response.status}`);
|
1182 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1183 |
}
|
1184 |
+
|
1185 |
+
// End streaming after successful completion
|
1186 |
+
appendMessage("", 'bot', false);
|
1187 |
+
|
1188 |
+
} catch (error) {
|
1189 |
+
console.error("API call error:", error);
|
1190 |
+
if (typingIndicator) {
|
1191 |
+
typingIndicator.remove();
|
1192 |
+
}
|
1193 |
+
throw error;
|
1194 |
}
|
1195 |
+
}
|
1196 |
|
1197 |
// Toggle theme
|
1198 |
function toggleTheme() {
|