ParthSadaria commited on
Commit
8687327
·
verified ·
1 Parent(s): 4bcb2c2

Update playground.html

Browse files
Files changed (1) hide show
  1. 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
- 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: '#10B981',
50
- dark: '#059669',
51
- light: '#6EE7B7',
52
- },
53
- dark: {
54
- DEFAULT: '#0F172A', // Darker blue-black
55
- lighter: '#1E293B', // Slate 800
56
- card: '#1E293B', // Slate 800
57
- input: '#334155', // Slate 700
58
- },
59
- light: {
60
- DEFAULT: '#F8FAFC',
61
- darker: '#E2E8F0',
62
- card: '#FFFFFF',
63
- input: '#F1F5F9',
64
- }
65
- },
66
- animation: {
67
- 'bounce-slow': 'bounce 1.5s infinite',
68
- 'typing': 'typing 1s infinite',
69
- 'fade-in': 'fadeIn 0.5s ease-in-out',
70
- },
71
- keyframes: {
72
- typing: {
73
- '0%, 100%': { opacity: 0 },
74
- '50%': { opacity: 1 },
75
- },
76
- fadeIn: {
77
- '0%': { opacity: 0 },
78
- '100%': { opacity: 1 },
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-semibold bg-gradient-to-r from-primary to-primary-light bg-clip-text text-transparent">Welcome to LOKI.AI</h2>
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" id="sendIcon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
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-xs text-gray-500 font-mono">
577
- Made with <span class="text-primary-light">❤️</span> by Parth Sadaria
578
  </div>
579
 
580
  <script>
@@ -826,150 +829,140 @@ function populateModels(models) {
826
  }
827
 
828
  // Append message to chat
829
- function appendMessage(text, sender, isStreaming = false) {
830
- const now = new Date();
831
- const messageTime = formatTimestamp(now);
832
-
833
- if (isStreaming) {
834
- if (!currentStreamingMessage) {
835
- // Create new message container for streaming
836
- isStreamingInProgress = true;
837
- lastMessageTime = now;
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 = 'message-appear flex flex-col items-start';
916
-
917
- if (sender === 'user') {
918
- messageDiv.classList.add('items-end');
919
- }
920
-
921
  const messageBubble = document.createElement('div');
922
- 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'}`;
923
-
924
- // Content div with markdown support
 
 
 
925
  const contentDiv = document.createElement('div');
926
  contentDiv.className = 'markdown-content break-words';
 
927
  contentDiv.innerHTML = marked.parse(text);
928
-
929
- // Apply syntax highlighting to code blocks
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
- // Scroll to bottom if auto-scroll is enabled
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
- let typingIndicator = null;
1043
-
1044
- try {
1045
- // Add typing indicator
1046
- typingIndicator = document.createElement('div');
1047
- typingIndicator.className = 'p-3 rounded-lg bg-gray-100 dark:bg-gray-800 mr-8 flex space-x-1';
1048
- typingIndicator.innerHTML = `
1049
- <div class="w-2 h-2 bg-gray-500 rounded-full animate-typing"></div>
1050
- <div class="w-2 h-2 bg-gray-500 rounded-full animate-typing" style="animation-delay: 0.2s"></div>
1051
- <div class="w-2 h-2 bg-gray-500 rounded-full animate-typing" style="animation-delay: 0.4s"></div>
1052
- `;
1053
- chatMessages.appendChild(typingIndicator);
1054
- scrollToBottom();
 
 
 
 
 
 
 
 
 
1055
 
1056
- if (model === "searchgpt") {
1057
- 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`;
 
 
 
 
1058
 
1059
- const response = await fetch(url, {
1060
- headers: {
1061
- 'Authorization': 'playground'
1062
- }
1063
- });
1064
 
1065
- if (response.ok) {
1066
- // Remove typing indicator
1067
- if (typingIndicator) {
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
- while (!done) {
1077
- const { value, done: streamDone } = await reader.read();
1078
- done = streamDone;
1079
 
1080
- if (value) {
1081
- const chunk = decoder.decode(value);
1082
- const cleanedChunk = chunk.trim();
1083
 
1084
- if (cleanedChunk.startsWith('data:')) {
1085
- const jsonChunks = cleanedChunk.split("data:").filter(Boolean);
1086
-
1087
- for (const jsonString of jsonChunks) {
1088
- try {
1089
- const cleanJson = jsonString.trim();
1090
- if (cleanJson === '[DONE]') continue;
1091
-
1092
- const jsonData = JSON.parse(cleanJson);
1093
- const content = jsonData.choices?.[0]?.message?.content ||
1094
- jsonData.choices?.[0]?.delta?.content || "";
1095
-
1096
- if (content) {
 
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
- const url = "https://parthsadaria-lokiai.hf.space/chat/completions";
1113
- const payload = {
1114
- model: model,
1115
- messages: [...conversationHistory],
1116
- stream: true
1117
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1118
 
1119
- const response = await fetch(url, {
1120
- method: "POST",
1121
- headers: {
1122
- "Content-Type": "application/json",
1123
- "Authorization": "playground"
1124
- },
1125
- body: JSON.stringify(payload)
1126
- });
1127
 
1128
- if (response.ok) {
1129
- // Remove typing indicator
1130
- if (typingIndicator) {
1131
- typingIndicator.remove();
1132
- typingIndicator = null;
1133
- }
1134
 
1135
- const reader = response.body.getReader();
1136
- const decoder = new TextDecoder("utf-8");
1137
- let done = false;
1138
- let buffer = "";
1139
-
1140
- while (!done) {
1141
- const { value, done: streamDone } = await reader.read();
1142
- done = streamDone;
1143
 
1144
- if (value) {
1145
- const chunk = decoder.decode(value);
1146
- buffer += chunk;
1147
 
1148
- const lines = buffer.split('\n');
1149
- buffer = lines.pop() || "";
1150
 
1151
- for (const line of lines) {
1152
- if (line.trim() === '') continue;
 
1153
 
1154
- const cleanedLine = line.replace(/^data:\s*/, '').trim();
1155
- if (cleanedLine === '[DONE]') continue;
1156
-
1157
- try {
1158
- const jsonData = JSON.parse(cleanedLine);
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() {