caiosilva1221 commited on
Commit
bdba398
·
verified ·
1 Parent(s): 0c97c9d

Update src/components/ask-ai/ask-ai.tsx

Browse files
Files changed (1) hide show
  1. src/components/ask-ai/ask-ai.tsx +64 -67
src/components/ask-ai/ask-ai.tsx CHANGED
@@ -1,13 +1,11 @@
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { useEffect, useState, useRef } from "react";
3
  import { RiSparkling2Fill } from "react-icons/ri";
4
  import { GrSend } from "react-icons/gr";
5
- import classNames from "classnames";
6
- import { toast } from "react-toastify";
7
  import { MdPreview } from "react-icons/md";
8
-
9
- import { defaultHTML } from "../../../utils/consts";
10
- import SuccessSound from "../../assets/success.mp3";
11
 
12
  function AskAI({
13
  html,
@@ -29,116 +27,115 @@ function AskAI({
29
  const [prompt, setPrompt] = useState("");
30
  const [hasAsked, setHasAsked] = useState(false);
31
  const [previousPrompt, setPreviousPrompt] = useState("");
32
- const controllerRef = useRef<AbortController | null>(null);
33
- const audio = useRef(new Audio(SuccessSound));
 
34
 
35
  const callAi = async () => {
36
  if (isAiWorking || !prompt.trim()) return;
37
  setisAiWorking(true);
38
 
39
- controllerRef.current?.abort();
40
- controllerRef.current = new AbortController();
41
-
42
- const signal = controllerRef.current.signal;
43
 
44
  try {
45
  onNewPrompt(prompt);
46
 
47
- const response = await fetch("/api/ask-ai", {
48
  method: "POST",
49
- headers: { "Content-Type": "application/json" },
 
 
50
  body: JSON.stringify({
51
  prompt,
52
  previousPrompt,
53
  ...(html === defaultHTML ? {} : { html }),
54
  }),
55
- signal,
56
  });
57
 
58
- if (!response.ok || !response.body) {
59
  toast.error("Erro ao chamar a IA.");
60
  setisAiWorking(false);
61
  return;
62
  }
63
 
64
- const reader = response.body.getReader();
65
  const decoder = new TextDecoder("utf-8");
66
- let completeResponse = "";
67
- let lastRenderTime = 0;
68
 
69
- while (true) {
70
  const { done, value } = await reader.read();
71
- if (done) break;
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  const chunk = decoder.decode(value, { stream: true });
74
- const lines = chunk.split("\n").filter((line) => line.startsWith("data: "));
75
-
76
- for (const line of lines) {
77
- try {
78
- const json = JSON.parse(line.slice(6));
79
- const content = json?.choices?.[0]?.delta?.content;
80
- if (content) {
81
- completeResponse += content;
82
-
83
- const now = Date.now();
84
- if (now - lastRenderTime > 300) {
85
- let partial = completeResponse.match(/<!DOCTYPE html>[\s\S]*/)?.[0];
86
- if (partial && !partial.includes("</html>")) partial += "</html>";
87
- if (partial) setHtml(partial);
88
- lastRenderTime = now;
89
- }
90
- if (completeResponse.length > 200) onScrollToBottom();
91
- }
92
- } catch (e) {
93
- continue;
94
  }
 
 
95
  }
96
- }
97
 
98
- const finalDoc = completeResponse.match(/<!DOCTYPE html>[\s\S]*<\/html>/)?.[0];
99
- if (finalDoc) setHtml(finalDoc);
100
- setPrompt("");
101
- setPreviousPrompt(prompt);
102
- setisAiWorking(false);
103
- setHasAsked(true);
104
- setView("preview");
105
- setTimeout(() => audio.current.play(), 0);
106
  } catch (error: any) {
107
- if (error.name !== "AbortError") {
108
- toast.error(error.message || "Erro inesperado");
109
- }
110
  setisAiWorking(false);
 
111
  }
112
  };
113
 
114
  return (
115
- <div className={`bg-gray-950 rounded-xl py-2 px-4 absolute bottom-3 left-3 w-[calc(100%-1.5rem)] z-10 group ${isAiWorking ? "animate-pulse" : ""}`}>
116
  {defaultHTML !== html && (
117
  <button
118
- className="bg-white text-gray-950 text-xs font-medium py-2 px-3 rounded-lg flex items-center gap-2 border border-gray-100 hover:brightness-150"
119
  onClick={() => setView("preview")}
120
  >
121
- <MdPreview className="text-sm" /> View Preview
 
122
  </button>
123
  )}
124
- <div className="flex items-center justify-between">
125
- <RiSparkling2Fill className="text-xl text-gray-500 group-focus-within:text-pink-500" />
126
  <input
127
  type="text"
128
  disabled={isAiWorking}
129
- className="w-full bg-transparent text-sm outline-none px-3 text-white placeholder:text-gray-500"
130
  placeholder={hasAsked ? "O que você quer criar agora?" : "Digite sua ideia aqui..."}
131
  value={prompt}
132
  onChange={(e) => setPrompt(e.target.value)}
133
- onKeyDown={(e) => e.key === "Enter" && callAi()}
134
  />
135
- <button
136
- disabled={isAiWorking}
137
- className="rounded-full bg-pink-500 text-white size-8 flex items-center justify-center disabled:bg-gray-300"
138
- onClick={callAi}
139
- >
140
- <GrSend className="-translate-x-[1px]" />
141
- </button>
 
 
142
  </div>
143
  </div>
144
  );
 
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { useState } from "react";
3
  import { RiSparkling2Fill } from "react-icons/ri";
4
  import { GrSend } from "react-icons/gr";
 
 
5
  import { MdPreview } from "react-icons/md";
6
+ import { toast } from "react-toastify";
7
+ import { defaultHTML } from "./../../../utils/consts";
8
+ import SuccessSound from "./../../assets/success.mp3";
9
 
10
  function AskAI({
11
  html,
 
27
  const [prompt, setPrompt] = useState("");
28
  const [hasAsked, setHasAsked] = useState(false);
29
  const [previousPrompt, setPreviousPrompt] = useState("");
30
+
31
+ const audio = new Audio(SuccessSound);
32
+ audio.volume = 0.5;
33
 
34
  const callAi = async () => {
35
  if (isAiWorking || !prompt.trim()) return;
36
  setisAiWorking(true);
37
 
38
+ let contentResponse = "";
39
+ let lastRenderTime = 0;
 
 
40
 
41
  try {
42
  onNewPrompt(prompt);
43
 
44
+ const request = await fetch("/api/ask-ai", {
45
  method: "POST",
46
+ headers: {
47
+ "Content-Type": "application/json",
48
+ },
49
  body: JSON.stringify({
50
  prompt,
51
  previousPrompt,
52
  ...(html === defaultHTML ? {} : { html }),
53
  }),
 
54
  });
55
 
56
+ if (!request.ok || !request.body) {
57
  toast.error("Erro ao chamar a IA.");
58
  setisAiWorking(false);
59
  return;
60
  }
61
 
62
+ const reader = request.body.getReader();
63
  const decoder = new TextDecoder("utf-8");
 
 
64
 
65
+ const read = async () => {
66
  const { done, value } = await reader.read();
67
+ if (done) {
68
+ toast.success("Página gerada com sucesso!");
69
+ setPrompt("");
70
+ setPreviousPrompt(prompt);
71
+ setisAiWorking(false);
72
+ setHasAsked(true);
73
+ audio.play();
74
+ setView("preview");
75
+
76
+ const finalDoc = contentResponse.match(/<!DOCTYPE html>[\s\S]*<\/html>/)?.[0];
77
+ if (finalDoc) setHtml(finalDoc);
78
+ return;
79
+ }
80
 
81
  const chunk = decoder.decode(value, { stream: true });
82
+ contentResponse += chunk;
83
+
84
+ const newHtml = contentResponse.match(/<!DOCTYPE html>[\s\S]*/)?.[0];
85
+ if (newHtml) {
86
+ let partialDoc = newHtml;
87
+ if (!partialDoc.includes("</html>")) partialDoc += "\n</html>";
88
+
89
+ const now = Date.now();
90
+ if (now - lastRenderTime > 300) {
91
+ setHtml(partialDoc);
92
+ lastRenderTime = now;
 
 
 
 
 
 
 
 
 
93
  }
94
+
95
+ if (partialDoc.length > 200) onScrollToBottom();
96
  }
 
97
 
98
+ read();
99
+ };
100
+
101
+ read();
 
 
 
 
102
  } catch (error: any) {
 
 
 
103
  setisAiWorking(false);
104
+ toast.error("Erro: " + error.message);
105
  }
106
  };
107
 
108
  return (
109
+ <div className={`bg-gray-950 rounded-xl py-2 lg:py-2.5 pl-3.5 lg:pl-4 pr-2 lg:pr-2.5 absolute lg:sticky bottom-3 left-3 lg:bottom-4 lg:left-4 w-[calc(100%-1.5rem)] lg:w-[calc(100%-2rem)] z-10 group ${isAiWorking ? "animate-pulse" : ""}`}>
110
  {defaultHTML !== html && (
111
  <button
112
+ className="bg-white lg:hidden -translate-y-[calc(100%+8px)] absolute left-0 top-0 shadow-md text-gray-950 text-xs font-medium py-2 px-3 lg:px-4 rounded-lg flex items-center gap-2 border border-gray-100 hover:brightness-150 transition-all duration-100 cursor-pointer"
113
  onClick={() => setView("preview")}
114
  >
115
+ <MdPreview className="text-sm" />
116
+ Ver Prévia
117
  </button>
118
  )}
119
+ <div className="w-full relative flex items-center justify-between">
120
+ <RiSparkling2Fill className="text-lg lg:text-xl text-gray-500 group-focus-within:text-pink-500" />
121
  <input
122
  type="text"
123
  disabled={isAiWorking}
124
+ className="w-full bg-transparent max-lg:text-sm outline-none px-3 text-white placeholder:text-gray-500 font-code"
125
  placeholder={hasAsked ? "O que você quer criar agora?" : "Digite sua ideia aqui..."}
126
  value={prompt}
127
  onChange={(e) => setPrompt(e.target.value)}
128
+ onKeyDown={(e) => { if (e.key === "Enter") callAi(); }}
129
  />
130
+ <div className="flex items-center justify-end gap-2">
131
+ <button
132
+ disabled={isAiWorking}
133
+ className="relative overflow-hidden cursor-pointer flex-none flex items-center justify-center rounded-full text-sm font-semibold size-8 text-center bg-pink-500 hover:bg-pink-400 text-white shadow-sm disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed"
134
+ onClick={callAi}
135
+ >
136
+ <GrSend className="-translate-x-[1px]" />
137
+ </button>
138
+ </div>
139
  </div>
140
  </div>
141
  );