Browse Source

Merge pull request #236 from drew-royster/feat/error-toast

Add error toast in tinychat
Alex Cheema 7 months ago
parent
commit
bbb354e5ec

+ 14 - 0
tinychat/examples/tinychat/index.css

@@ -169,6 +169,20 @@ main {
   white-space: pre-wrap;
 }
 
+.toast {
+    width: 100%; /* Take up the full width of the page */
+    background-color: #fc2a2a; /* Dark background color */
+    color: #fff; /* White text color */
+    text-align: center; /* Centered text */
+    border-radius: 2px; /* Rounded borders */
+    padding: 16px; /* Padding */
+    position: fixed; /* Sit on top of the screen content */
+    z-index: 9999; /* Add a z-index if needed */
+    top: 0; /* Position at the top of the page */
+    left: 0; /* Extend from the left edge */
+    right: 0; /* Extend to the right edge */
+}
+
 .hljs {
   width: 100%;
   position: relative;

+ 3 - 0
tinychat/examples/tinychat/index.html

@@ -25,6 +25,9 @@
 </link></head>
 <body>
 <main x-data="state" x-init="console.log(endpoint)">
+     <!-- Error Toast -->
+    <div x-show="errorMessage" x-transition.opacity x-text="errorMessage" class="toast">
+    </div>
 <div class="model-selector">
 <select @change="if (cstate) cstate.selectedModel = $event.target.value" x-model="cstate.selectedModel">
 <option selected="" value="llama-3.2-1b">Llama 3.2 1B</option>

+ 122 - 108
tinychat/examples/tinychat/index.js

@@ -13,6 +13,7 @@ document.addEventListener("alpine:init", () => {
     home: 0,
     generating: false,
     endpoint: `${window.location.origin}/v1`,
+    errorMessage: null,
 
     // performance tracking
     time_till_first: 0,
@@ -51,138 +52,146 @@ document.addEventListener("alpine:init", () => {
 
 
     async handleSend() {
-      const el = document.getElementById("input-form");
-      const value = el.value.trim();
-      if (!value && !this.imagePreview) return;
+      try {
+        const el = document.getElementById("input-form");
+        const value = el.value.trim();
+        if (!value && !this.imagePreview) return;
 
-      if (this.generating) return;
-      this.generating = true;
-      if (this.home === 0) this.home = 1;
+        if (this.generating) return;
+        this.generating = true;
+        if (this.home === 0) this.home = 1;
 
-      // ensure that going back in history will go back to home
-      window.history.pushState({}, "", "/");
+        // ensure that going back in history will go back to home
+        window.history.pushState({}, "", "/");
 
-      // add message to list
-      if (value) {
-        this.cstate.messages.push({ role: "user", content: value });
-      }
+        // add message to list
+        if (value) {
+          this.cstate.messages.push({ role: "user", content: value });
+        }
 
-      // clear textarea
-      el.value = "";
-      el.style.height = "auto";
-      el.style.height = el.scrollHeight + "px";
+        // clear textarea
+        el.value = "";
+        el.style.height = "auto";
+        el.style.height = el.scrollHeight + "px";
 
-      // reset performance tracking
-      const prefill_start = Date.now();
-      let start_time = 0;
-      let tokens = 0;
-      this.tokens_per_second = 0;
+        // reset performance tracking
+        const prefill_start = Date.now();
+        let start_time = 0;
+        let tokens = 0;
+        this.tokens_per_second = 0;
 
-      // prepare messages for API request
-      let apiMessages = this.cstate.messages.map(msg => {
-        if (msg.content.startsWith('![Uploaded Image]')) {
-          return {
-            role: "user",
-            content: [
-              {
-                type: "image_url",
-                image_url: {
-                  url: this.imageUrl
-                }
-              },
-              {
-                type: "text",
-                text: value // Use the actual text the user typed
-              }
-            ]
-          };
-        } else {
-          return {
-            role: msg.role,
-            content: msg.content
-          };
-        }
-      });
-      const containsImage = apiMessages.some(msg => Array.isArray(msg.content) && msg.content.some(item => item.type === 'image_url'));
-      if (containsImage) {
-        // Map all messages with string content to object with type text
-        apiMessages = apiMessages.map(msg => {
-          if (typeof msg.content === 'string') {
+        // prepare messages for API request
+        let apiMessages = this.cstate.messages.map(msg => {
+          if (msg.content.startsWith('![Uploaded Image]')) {
             return {
-              ...msg,
+              role: "user",
               content: [
+                {
+                  type: "image_url",
+                  image_url: {
+                    url: this.imageUrl
+                  }
+                },
                 {
                   type: "text",
-                  text: msg.content
+                  text: value // Use the actual text the user typed
                 }
               ]
             };
+          } else {
+            return {
+              role: msg.role,
+              content: msg.content
+            };
           }
-          return msg;
         });
-      }
+        const containsImage = apiMessages.some(msg => Array.isArray(msg.content) && msg.content.some(item => item.type === 'image_url'));
+        if (containsImage) {
+          // Map all messages with string content to object with type text
+          apiMessages = apiMessages.map(msg => {
+            if (typeof msg.content === 'string') {
+              return {
+                ...msg,
+                content: [
+                  {
+                    type: "text",
+                    text: msg.content
+                  }
+                ]
+              };
+            }
+            return msg;
+          });
+        }
 
 
-      // start receiving server sent events
-      let gottenFirstChunk = false;
-      for await (
-        const chunk of this.openaiChatCompletion(this.cstate.selectedModel, apiMessages)
-      ) {
-        if (!gottenFirstChunk) {
-          this.cstate.messages.push({ role: "assistant", content: "" });
-          gottenFirstChunk = true;
-        }
+        // start receiving server sent events
+        let gottenFirstChunk = false;
+        for await (
+          const chunk of this.openaiChatCompletion(this.cstate.selectedModel, apiMessages)
+        ) {
+          if (!gottenFirstChunk) {
+            this.cstate.messages.push({ role: "assistant", content: "" });
+            gottenFirstChunk = true;
+          }
 
-        // add chunk to the last message
-        this.cstate.messages[this.cstate.messages.length - 1].content += chunk;
+          // add chunk to the last message
+          this.cstate.messages[this.cstate.messages.length - 1].content += chunk;
 
-        // calculate performance tracking
-        tokens += 1;
-        this.total_tokens += 1;
-        if (start_time === 0) {
-          start_time = Date.now();
-          this.time_till_first = start_time - prefill_start;
-        } else {
-          const diff = Date.now() - start_time;
-          if (diff > 0) {
-            this.tokens_per_second = tokens / (diff / 1000);
+          // calculate performance tracking
+          tokens += 1;
+          this.total_tokens += 1;
+          if (start_time === 0) {
+            start_time = Date.now();
+            this.time_till_first = start_time - prefill_start;
+          } else {
+            const diff = Date.now() - start_time;
+            if (diff > 0) {
+              this.tokens_per_second = tokens / (diff / 1000);
+            }
           }
         }
-      }
 
-      // Clean the cstate before adding it to histories
-      const cleanedCstate = JSON.parse(JSON.stringify(this.cstate));
-      cleanedCstate.messages = cleanedCstate.messages.map(msg => {
-        if (Array.isArray(msg.content)) {
-          return {
-            ...msg,
-            content: msg.content.map(item =>
-              item.type === 'image_url' ? { type: 'image_url', image_url: { url: '[IMAGE_PLACEHOLDER]' } } : item
-            )
-          };
-        }
-        return msg;
-      });
+        // Clean the cstate before adding it to histories
+        const cleanedCstate = JSON.parse(JSON.stringify(this.cstate));
+        cleanedCstate.messages = cleanedCstate.messages.map(msg => {
+          if (Array.isArray(msg.content)) {
+            return {
+              ...msg,
+              content: msg.content.map(item =>
+                item.type === 'image_url' ? { type: 'image_url', image_url: { url: '[IMAGE_PLACEHOLDER]' } } : item
+              )
+            };
+          }
+          return msg;
+        });
 
-      // Update the state in histories or add it if it doesn't exist
-      const index = this.histories.findIndex((cstate) => cstate.time === cleanedCstate.time);
-      cleanedCstate.time = Date.now();
-      if (index !== -1) {
-        // Update the existing entry
-        this.histories[index] = cleanedCstate;
-      } else {
-        // Add a new entry
-        this.histories.push(cleanedCstate);
-      }
-      console.log(this.histories)
-      // update in local storage
-      try {
-        localStorage.setItem("histories", JSON.stringify(this.histories));
+        // Update the state in histories or add it if it doesn't exist
+        const index = this.histories.findIndex((cstate) => cstate.time === cleanedCstate.time);
+        cleanedCstate.time = Date.now();
+        if (index !== -1) {
+          // Update the existing entry
+          this.histories[index] = cleanedCstate;
+        } else {
+          // Add a new entry
+          this.histories.push(cleanedCstate);
+        }
+        console.log(this.histories)
+        // update in local storage
+        try {
+          localStorage.setItem("histories", JSON.stringify(this.histories));
+        } catch (error) {
+          console.error("Failed to save histories to localStorage:", error);
+        }
       } catch (error) {
-        console.error("Failed to save histories to localStorage:", error);
+        console.error('error', error)
+        this.errorMessage = error;
+        setTimeout(() => {
+          this.errorMessage = null;
+        }, 5 * 1000)
+      } finally {
+        this.generating = false;
       }
-
-      this.generating = false;
     },
 
     async handleEnter(event) {
@@ -218,7 +227,12 @@ document.addEventListener("alpine:init", () => {
         }),
       });
       if (!response.ok) {
-        throw new Error("Failed to fetch");
+        const errorResBody = await response.json()
+        if (errorResBody?.detail) {
+          throw new Error(`Failed to fetch completions: ${errorResBody.detail}`);
+        } else {
+          throw new Error("Failed to fetch completions: Unknown error");
+        }
       }
 
       const reader = response.body.pipeThrough(new TextDecoderStream())