caiosilva1221 commited on
Commit
c707f99
·
verified ·
1 Parent(s): 42690f6

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

Browse files
Files changed (1) hide show
  1. src/components/ask-ai/ask-ai.tsx +181 -51
src/components/ask-ai/ask-ai.tsx CHANGED
@@ -1,11 +1,17 @@
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,
@@ -24,9 +30,46 @@ function AskAI({
24
  setView: React.Dispatch<React.SetStateAction<"editor" | "preview">>;
25
  setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
26
  }) {
 
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;
@@ -34,79 +77,122 @@ function AskAI({
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,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"
@@ -122,23 +208,67 @@ function AskAI({
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
  );
142
  }
143
 
144
- export default AskAI;
 
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { useEffect, useState } 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 { useLocalStorage } from "react-use";
8
+ import { MdPreview } from "react-icons/md";
9
+
10
+ import Login from "../login/login";
11
  import { defaultHTML } from "./../../../utils/consts";
12
  import SuccessSound from "./../../assets/success.mp3";
13
+ import Settings from "../settings/settings";
14
+ import ProModal from "../pro-modal/pro-modal";
15
 
16
  function AskAI({
17
  html,
 
30
  setView: React.Dispatch<React.SetStateAction<"editor" | "preview">>;
31
  setisAiWorking: React.Dispatch<React.SetStateAction<boolean>>;
32
  }) {
33
+ const [open, setOpen] = useState(false);
34
  const [prompt, setPrompt] = useState("");
35
  const [hasAsked, setHasAsked] = useState(false);
36
  const [previousPrompt, setPreviousPrompt] = useState("");
37
+ const [provider, setProvider] = useLocalStorage("provider", "auto");
38
+ const [openProvider, setOpenProvider] = useState(false);
39
+ const [providerError, setProviderError] = useState("");
40
+ const [openProModal, setOpenProModal] = useState(false);
41
+ const [localSettings, setLocalSettings] = useState(() => {
42
+ const saved = localStorage.getItem('localSettings');
43
+ return saved ? JSON.parse(saved) : {
44
+ apiKey: "",
45
+ apiUrl: "http://localhost:11434/v1",
46
+ model: "gemma3:1b",
47
+ openRouterApiKey: "<OPENROUTER_API_KEY>",
48
+ openRouterApiUrl: "https://openrouter.ai/api/v1",
49
+ openRouterModel: "deepseek/deepseek-chat-v3-0324:free",
50
+ };
51
+ });
52
+
53
+ const loadLocalSettings = () => {
54
+ const saved = localStorage.getItem('localSettings');
55
+ if (saved) {
56
+ const parsed = JSON.parse(saved);
57
+ setLocalSettings(parsed);
58
+ } else {
59
+ setLocalSettings({
60
+ apiKey: "",
61
+ apiUrl: "http://localhost:11434/v1",
62
+ model: "gemma3:1b",
63
+ openRouterApiKey: "<OPENROUTER_API_KEY>",
64
+ openRouterApiUrl: "https://openrouter.ai/api/v1",
65
+ openRouterModel: "deepseek/deepseek-chat-v3-0324:free",
66
+ });
67
+ }
68
+ };
69
+
70
+ useEffect(() => {
71
+ loadLocalSettings();
72
+ }, []);
73
 
74
  const audio = new Audio(SuccessSound);
75
  audio.volume = 0.5;
 
77
  const callAi = async () => {
78
  if (isAiWorking || !prompt.trim()) return;
79
  setisAiWorking(true);
80
+ setProviderError("");
81
 
82
  let contentResponse = "";
83
  let lastRenderTime = 0;
 
84
  try {
85
  onNewPrompt(prompt);
 
86
  const request = await fetch("/api/ask-ai", {
87
  method: "POST",
 
 
 
88
  body: JSON.stringify({
89
  prompt,
90
+ provider,
91
+ ...(provider === "local"
92
+ ? {
93
+ ApiKey: localSettings.apiKey,
94
+ ApiUrl: localSettings.apiUrl,
95
+ Model: localSettings.model,
96
+ }
97
+ : {}),
98
+ ...(provider === "openrouter"
99
+ ? {
100
+ ApiKey: localSettings.openRouterApiKey,
101
+ ApiUrl: localSettings.openRouterApiUrl,
102
+ Model: localSettings.openRouterModel,
103
+ }
104
+ : {}),
105
+ ...(html === defaultHTML ? {} : { html }),
106
+ ...(previousPrompt ? { previousPrompt } : {}),
107
  }),
108
+ headers: {
109
+ "Content-Type": "application/json",
110
+ },
111
  });
112
+ if (request && request.body) {
113
+ if (!request.ok) {
114
+ const res = await request.json();
115
+ if (res.openLogin) {
116
+ setOpen(true);
117
+ } else if (res.openSelectProvider) {
118
+ setOpenProvider(true);
119
+ setProviderError(res.message);
120
+ } else if (res.openProModal) {
121
+ setOpenProModal(true);
122
+ } else {
123
+ toast.error(res.message);
124
+ }
 
 
 
125
  setisAiWorking(false);
 
 
 
 
 
 
126
  return;
127
  }
128
+ const reader = request.body.getReader();
129
+ const decoder = new TextDecoder("utf-8");
130
 
131
+ const read = async () => {
132
+ const { done, value } = await reader.read();
133
+ if (done) {
134
+ toast.success("AI responded successfully");
135
+ setPrompt("");
136
+ setPreviousPrompt(prompt);
137
+ setisAiWorking(false);
138
+ setHasAsked(true);
139
+ audio.play();
140
+ setView("preview");
141
 
142
+ // Now we have the complete HTML including </html>, so set it to be sure
143
+ const finalDoc = contentResponse.match(
144
+ /<!DOCTYPE html>[\s\S]*<\/html>/
145
+ )?.[0];
146
+ if (finalDoc) {
147
+ setHtml(finalDoc);
148
+ }
149
 
150
+ return;
 
 
 
151
  }
152
 
153
+ const chunk = decoder.decode(value, { stream: true });
154
+ contentResponse += chunk;
155
+ const newHtml = contentResponse.match(/<!DOCTYPE html>[\s\S]*/)?.[0];
156
+ if (newHtml) {
157
+ // Force-close the HTML tag so the iframe doesn't render half-finished markup
158
+ let partialDoc = newHtml;
159
+ if (!partialDoc.includes("</html>")) {
160
+ partialDoc += "\n</html>";
161
+ }
162
+
163
+ // Throttle the re-renders to avoid flashing/flicker
164
+ const now = Date.now();
165
+ if (now - lastRenderTime > 300) {
166
+ setHtml(partialDoc);
167
+ lastRenderTime = now;
168
+ }
169
+
170
+ if (partialDoc.length > 200) {
171
+ onScrollToBottom();
172
+ }
173
+ }
174
+ read();
175
+ };
176
 
177
  read();
178
+ }
179
 
180
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
  } catch (error: any) {
182
  setisAiWorking(false);
183
+ toast.error(error.message);
184
+ if (error.openLogin) {
185
+ setOpen(true);
186
+ }
187
  }
188
  };
189
 
190
  return (
191
+ <div
192
+ 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 ${
193
+ isAiWorking ? "animate-pulse" : ""
194
+ }`}
195
+ >
196
  {defaultHTML !== html && (
197
  <button
198
  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"
 
208
  type="text"
209
  disabled={isAiWorking}
210
  className="w-full bg-transparent max-lg:text-sm outline-none px-3 text-white placeholder:text-gray-500 font-code"
211
+ placeholder={
212
+ hasAsked ? "What do you want to ask AI next?" : "Ask AI anything..."
213
+ }
214
  value={prompt}
215
  onChange={(e) => setPrompt(e.target.value)}
216
+ onKeyDown={(e) => {
217
+ if (e.key === "Enter") {
218
+ callAi();
219
+ }
220
+ }}
221
  />
222
  <div className="flex items-center justify-end gap-2">
223
+ {/* <SpeechPrompt setPrompt={setPrompt} /> */}
224
+ <Settings
225
+ provider={provider as string}
226
+ onChange={setProvider}
227
+ open={openProvider}
228
+ error={providerError}
229
+ onClose={setOpenProvider}
230
+ setLocalSettings={setLocalSettings}
231
+ localSettings={localSettings}
232
+ />
233
  <button
234
  disabled={isAiWorking}
235
+ 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 dark:shadow-highlight/20 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed disabled:hover:bg-gray-300"
236
  onClick={callAi}
237
  >
238
  <GrSend className="-translate-x-[1px]" />
239
  </button>
240
  </div>
241
  </div>
242
+ <div
243
+ className={classNames(
244
+ "h-screen w-screen bg-black/20 fixed left-0 top-0 z-10",
245
+ {
246
+ "opacity-0 pointer-events-none": !open,
247
+ }
248
+ )}
249
+ onClick={() => setOpen(false)}
250
+ ></div>
251
+ <div
252
+ className={classNames(
253
+ "absolute top-0 -translate-y-[calc(100%+8px)] right-0 z-10 w-80 bg-white border border-gray-200 rounded-lg shadow-lg transition-all duration-75 overflow-hidden",
254
+ {
255
+ "opacity-0 pointer-events-none": !open,
256
+ }
257
+ )}
258
+ >
259
+ <Login html={html}>
260
+ <p className="text-gray-500 text-sm mb-3">
261
+ You reached the limit of free AI usage. Please login to continue.
262
+ </p>
263
+ </Login>
264
+ </div>
265
+ <ProModal
266
+ html={html}
267
+ open={openProModal}
268
+ onClose={() => setOpenProModal(false)}
269
+ />
270
  </div>
271
  );
272
  }
273
 
274
+ export default AskAI;