enzostvs HF Staff commited on
Commit
c95c69f
·
1 Parent(s): b271459

show a mention design for image link

Browse files
app/api/ask/route.ts CHANGED
@@ -90,7 +90,7 @@ export async function POST(request: Request) {
90
  )}\n`
91
  : ""
92
  }`,
93
- },
94
  ],
95
  stream: true,
96
  max_tokens: 16_000,
 
90
  )}\n`
91
  : ""
92
  }`,
93
+ }
94
  ],
95
  stream: true,
96
  max_tokens: 16_000,
components/ask-ai/ask-ai.tsx CHANGED
@@ -54,6 +54,7 @@ export function AskAI({
54
  url: string;
55
  } | null>(null);
56
  const [selectedMedias, setSelectedMedias] = useState<string[]>([]);
 
57
  const [startTour, setStartTour] = useState<boolean>(false);
58
 
59
  const router = useRouter();
@@ -95,11 +96,12 @@ export function AskAI({
95
  onComplete,
96
  provider,
97
  redesignMd,
98
- medias: selectedMedias ?? [],
99
  },
100
  setModel
101
  );
102
  if (selectedMedias.length > 0) setSelectedMedias([]);
 
103
  if (redesignMd) setRedesignMd(null);
104
  };
105
 
@@ -118,6 +120,8 @@ export function AskAI({
118
  setPrompt={setPrompt}
119
  redesignMdUrl={redesignMd?.url?.replace(/(^\w+:|^)\/\//, "")}
120
  onSubmit={onSubmit}
 
 
121
  />
122
  <footer className="flex items-center justify-between mt-0">
123
  <div className="flex items-center gap-1.5">
 
54
  url: string;
55
  } | null>(null);
56
  const [selectedMedias, setSelectedMedias] = useState<string[]>([]);
57
+ const [imageLinks, setImageLinks] = useState<string[]>([]);
58
  const [startTour, setStartTour] = useState<boolean>(false);
59
 
60
  const router = useRouter();
 
96
  onComplete,
97
  provider,
98
  redesignMd,
99
+ medias: [...(selectedMedias ?? []), ...(imageLinks ?? [])],
100
  },
101
  setModel
102
  );
103
  if (selectedMedias.length > 0) setSelectedMedias([]);
104
+ if (imageLinks.length > 0) setImageLinks([]);
105
  if (redesignMd) setRedesignMd(null);
106
  };
107
 
 
120
  setPrompt={setPrompt}
121
  redesignMdUrl={redesignMd?.url?.replace(/(^\w+:|^)\/\//, "")}
122
  onSubmit={onSubmit}
123
+ imageLinks={imageLinks}
124
+ setImageLinks={setImageLinks}
125
  />
126
  <footer className="flex items-center justify-between mt-0">
127
  <div className="flex items-center gap-1.5">
components/ask-ai/input-mentions.tsx CHANGED
@@ -13,6 +13,8 @@ export function InputMentions({
13
  setPrompt,
14
  redesignMdUrl,
15
  onSubmit,
 
 
16
  }: {
17
  ref: RefObject<HTMLDivElement | null>;
18
  prompt: string;
@@ -20,6 +22,8 @@ export function InputMentions({
20
  redesignMdUrl?: string;
21
  setPrompt: (prompt: string) => void;
22
  onSubmit: () => void;
 
 
23
  }) {
24
  const queryClient = useQueryClient();
25
  const [showMentionDropdown, setShowMentionDropdown] = useState(false);
@@ -31,6 +35,12 @@ export function InputMentions({
31
  setShowMentionDropdown(false);
32
  });
33
 
 
 
 
 
 
 
34
  const getTextContent = (element: HTMLElement): string => {
35
  let text = "";
36
  const childNodes = element.childNodes;
@@ -43,6 +53,10 @@ export function InputMentions({
43
  const el = node as HTMLElement;
44
  if (el.classList.contains("mention-chip")) {
45
  text += el.getAttribute("data-mention-id") || "";
 
 
 
 
46
  } else {
47
  text += el.textContent || "";
48
  }
@@ -65,6 +79,10 @@ export function InputMentions({
65
  const el = node as HTMLElement;
66
  if (el.classList.contains("mention-chip")) {
67
  text += el.getAttribute("data-mention-id") || "";
 
 
 
 
68
  } else {
69
  text += el.textContent || "";
70
  }
@@ -73,6 +91,25 @@ export function InputMentions({
73
  return text;
74
  };
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  const shouldDetectMention = (): {
77
  detect: boolean;
78
  textBeforeCursor: string;
@@ -116,11 +153,23 @@ export function InputMentions({
116
  const handleInput = async () => {
117
  if (!ref.current) return;
118
  const text = getTextContent(ref.current);
119
- if (text.trim() === "") {
 
 
 
 
 
120
  ref.current.innerHTML = "";
121
  }
 
122
  setPrompt(text);
123
 
 
 
 
 
 
 
124
  const { detect, textBeforeCursor } = shouldDetectMention();
125
 
126
  if (detect && files && files?.length > 0) {
@@ -137,12 +186,8 @@ export function InputMentions({
137
  const createMentionChipElement = (mentionId: string): HTMLSpanElement => {
138
  const mentionChip = document.createElement("span");
139
 
140
- const baseClasses =
141
- "mention-chip inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium";
142
- const typeClasses =
143
- "bg-indigo-500/10 text-indigo-500 dark:bg-indigo-500/20 dark:text-indigo-400";
144
-
145
- mentionChip.className = `${baseClasses} ${typeClasses}`;
146
  mentionChip.contentEditable = "false";
147
  mentionChip.setAttribute("data-mention-id", `file:/${mentionId}`);
148
  mentionChip.textContent = `@${mentionId}`;
@@ -150,6 +195,30 @@ export function InputMentions({
150
  return mentionChip;
151
  };
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  const insertMention = (mentionId: string) => {
154
  if (!ref.current) return;
155
 
@@ -216,6 +285,12 @@ export function InputMentions({
216
  e.preventDefault();
217
  const promptWithIds = extractPromptWithIds();
218
  setPrompt(promptWithIds);
 
 
 
 
 
 
219
  onSubmit();
220
 
221
  if (ref.current) {
@@ -237,7 +312,92 @@ export function InputMentions({
237
  const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
238
  e.preventDefault();
239
  const text = e.clipboardData.getData("text/plain");
240
- document.execCommand("insertText", false, text);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  };
242
 
243
  return (
 
13
  setPrompt,
14
  redesignMdUrl,
15
  onSubmit,
16
+ imageLinks,
17
+ setImageLinks,
18
  }: {
19
  ref: RefObject<HTMLDivElement | null>;
20
  prompt: string;
 
22
  redesignMdUrl?: string;
23
  setPrompt: (prompt: string) => void;
24
  onSubmit: () => void;
25
+ imageLinks?: string[];
26
+ setImageLinks?: (links: string[]) => void;
27
  }) {
28
  const queryClient = useQueryClient();
29
  const [showMentionDropdown, setShowMentionDropdown] = useState(false);
 
35
  setShowMentionDropdown(false);
36
  });
37
 
38
+ const isImageUrl = (url: string): boolean => {
39
+ // Check if it's a valid HTTP/HTTPS URL with an image extension
40
+ const imageUrlPattern = /^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?.*)?$/i;
41
+ return imageUrlPattern.test(url);
42
+ };
43
+
44
  const getTextContent = (element: HTMLElement): string => {
45
  let text = "";
46
  const childNodes = element.childNodes;
 
53
  const el = node as HTMLElement;
54
  if (el.classList.contains("mention-chip")) {
55
  text += el.getAttribute("data-mention-id") || "";
56
+ } else if (el.classList.contains("image-chip")) {
57
+ // Include image URL in text content for display purposes
58
+ const imageUrl = el.getAttribute("data-image-url") || "";
59
+ text += imageUrl ? ` ${imageUrl} ` : "";
60
  } else {
61
  text += el.textContent || "";
62
  }
 
79
  const el = node as HTMLElement;
80
  if (el.classList.contains("mention-chip")) {
81
  text += el.getAttribute("data-mention-id") || "";
82
+ } else if (el.classList.contains("image-chip")) {
83
+ // Include image URL in prompt text
84
+ const imageUrl = el.getAttribute("data-image-url") || "";
85
+ text += imageUrl ? ` ${imageUrl} ` : "";
86
  } else {
87
  text += el.textContent || "";
88
  }
 
91
  return text;
92
  };
93
 
94
+ const extractImageLinks = (): string[] => {
95
+ if (!ref.current) return [];
96
+
97
+ const links: string[] = [];
98
+ const childNodes = ref.current.childNodes;
99
+
100
+ for (let i = 0; i < childNodes.length; i++) {
101
+ const node = childNodes[i];
102
+ if (node.nodeType === Node.ELEMENT_NODE) {
103
+ const el = node as HTMLElement;
104
+ if (el.classList.contains("image-chip")) {
105
+ const imageUrl = el.getAttribute("data-image-url");
106
+ if (imageUrl) links.push(imageUrl);
107
+ }
108
+ }
109
+ }
110
+ return links;
111
+ };
112
+
113
  const shouldDetectMention = (): {
114
  detect: boolean;
115
  textBeforeCursor: string;
 
153
  const handleInput = async () => {
154
  if (!ref.current) return;
155
  const text = getTextContent(ref.current);
156
+
157
+ // Only clear if there's no content at all (including chips)
158
+ const hasImageChips = ref.current.querySelectorAll(".image-chip").length > 0;
159
+ const hasMentionChips = ref.current.querySelectorAll(".mention-chip").length > 0;
160
+
161
+ if (text.trim() === "" && !hasImageChips && !hasMentionChips) {
162
  ref.current.innerHTML = "";
163
  }
164
+
165
  setPrompt(text);
166
 
167
+ // Update image links whenever input changes
168
+ if (setImageLinks) {
169
+ const links = extractImageLinks();
170
+ setImageLinks(links);
171
+ }
172
+
173
  const { detect, textBeforeCursor } = shouldDetectMention();
174
 
175
  if (detect && files && files?.length > 0) {
 
186
  const createMentionChipElement = (mentionId: string): HTMLSpanElement => {
187
  const mentionChip = document.createElement("span");
188
 
189
+ mentionChip.className =
190
+ "mention-chip inline-flex w-fit items-center justify-center gap-1 font-mono px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-500/10 text-indigo-500 dark:bg-indigo-500/20 dark:text-indigo-400";
 
 
 
 
191
  mentionChip.contentEditable = "false";
192
  mentionChip.setAttribute("data-mention-id", `file:/${mentionId}`);
193
  mentionChip.textContent = `@${mentionId}`;
 
195
  return mentionChip;
196
  };
197
 
198
+ const createImageChipElement = (imageUrl: string): HTMLSpanElement => {
199
+ const imageChip = document.createElement("span");
200
+
201
+ imageChip.className =
202
+ "image-chip inline-flex w-fit items-center justify-center gap-1 font-mono px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400";
203
+ imageChip.contentEditable = "false";
204
+ imageChip.setAttribute("data-image-url", imageUrl);
205
+
206
+ // Create icon (using emoji for simplicity)
207
+ const icon = document.createElement("span");
208
+ icon.textContent = "🖼️";
209
+ icon.className = "text-[10px]";
210
+
211
+ // Truncate URL for display
212
+ const displayUrl =
213
+ imageUrl.length > 30 ? imageUrl.substring(0, 30) + "..." : imageUrl;
214
+ const text = document.createTextNode(displayUrl);
215
+
216
+ imageChip.appendChild(icon);
217
+ imageChip.appendChild(text);
218
+
219
+ return imageChip;
220
+ };
221
+
222
  const insertMention = (mentionId: string) => {
223
  if (!ref.current) return;
224
 
 
285
  e.preventDefault();
286
  const promptWithIds = extractPromptWithIds();
287
  setPrompt(promptWithIds);
288
+
289
+ if (setImageLinks) {
290
+ const links = extractImageLinks();
291
+ setImageLinks(links);
292
+ }
293
+
294
  onSubmit();
295
 
296
  if (ref.current) {
 
312
  const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
313
  e.preventDefault();
314
  const text = e.clipboardData.getData("text/plain");
315
+
316
+ const selection = window.getSelection();
317
+ if (!selection || selection.rangeCount === 0 || !ref.current) {
318
+ document.execCommand("insertText", false, text);
319
+ return;
320
+ }
321
+
322
+ const trimmedText = text.trim();
323
+
324
+ // Check if pasted text is ONLY an image URL
325
+ if (isImageUrl(trimmedText)) {
326
+ const range = selection.getRangeAt(0);
327
+ const imageChip = createImageChipElement(trimmedText);
328
+ const spaceNode = document.createTextNode("\u0020");
329
+
330
+ range.deleteContents();
331
+ range.insertNode(imageChip);
332
+
333
+ // Insert space after the chip
334
+ const afterChipRange = document.createRange();
335
+ afterChipRange.setStartAfter(imageChip);
336
+ afterChipRange.insertNode(spaceNode);
337
+
338
+ // Move cursor after the space
339
+ const newRange = document.createRange();
340
+ newRange.setStartAfter(spaceNode);
341
+ newRange.collapse(true);
342
+ selection.removeAllRanges();
343
+ selection.addRange(newRange);
344
+
345
+ // Update state
346
+ const newText = getTextContent(ref.current);
347
+ setPrompt(newText);
348
+
349
+ if (setImageLinks) {
350
+ const links = extractImageLinks();
351
+ setImageLinks(links);
352
+ }
353
+ } else {
354
+ // Check if text contains image URLs
355
+ const imageUrlPattern = /https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?[^\s]*)?/gi;
356
+ const containsImageUrl = imageUrlPattern.test(text);
357
+
358
+ if (containsImageUrl) {
359
+ // Split text and replace image URLs with chips
360
+ const parts = text.split(/(https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?[^\s]*)?)/gi);
361
+ const range = selection.getRangeAt(0);
362
+ range.deleteContents();
363
+
364
+ const fragment = document.createDocumentFragment();
365
+
366
+ parts.forEach((part) => {
367
+ if (isImageUrl(part)) {
368
+ const imageChip = createImageChipElement(part);
369
+ const spaceNode = document.createTextNode("\u0020");
370
+ fragment.appendChild(imageChip);
371
+ fragment.appendChild(spaceNode);
372
+ } else if (part && !part.match(/^\?[^\s]*$/)) {
373
+ // Skip query string matches, add other text
374
+ const textNode = document.createTextNode(part);
375
+ fragment.appendChild(textNode);
376
+ }
377
+ });
378
+
379
+ range.insertNode(fragment);
380
+
381
+ // Move cursor to end
382
+ const newRange = document.createRange();
383
+ newRange.selectNodeContents(ref.current);
384
+ newRange.collapse(false);
385
+ selection.removeAllRanges();
386
+ selection.addRange(newRange);
387
+
388
+ // Update state
389
+ const newText = getTextContent(ref.current);
390
+ setPrompt(newText);
391
+
392
+ if (setImageLinks) {
393
+ const links = extractImageLinks();
394
+ setImageLinks(links);
395
+ }
396
+ } else {
397
+ // Plain text, use default insertion
398
+ document.execCommand("insertText", false, text);
399
+ }
400
+ }
401
  };
402
 
403
  return (
components/chat/index.tsx CHANGED
@@ -201,26 +201,49 @@ export function AppEditorChat({
201
  },
202
  pre: ({ children }) => <>{children}</>,
203
  p: ({ children }) => {
 
204
  if (
205
  typeof children === "string" &&
206
- String(children).includes("file:/")
 
207
  ) {
208
- const parts = children.split(/(file:\/\S+)/g);
209
  return (
210
  <p>
211
- {parts.map((part, index) =>
212
- part.startsWith("file:/") ? (
213
- <span
214
- key={index}
215
- className="inline-flex w-fit items-center justify-center gap-1 font-mono px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-500/10 text-indigo-500 dark:bg-indigo-500/20 dark:text-indigo-400"
216
- >
217
- {getFileIcon(part, "size-2.5")}
218
- {part.replace("file:/", "")}
219
- </span>
220
- ) : (
221
- part
222
- )
223
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  </p>
225
  );
226
  }
 
201
  },
202
  pre: ({ children }) => <>{children}</>,
203
  p: ({ children }) => {
204
+ const content = String(children);
205
  if (
206
  typeof children === "string" &&
207
+ (content.includes("file:/") ||
208
+ content.match(/https?:\/\/[^\s]+\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(?:\?[^\s]*)?/i))
209
  ) {
210
+ const parts = content.split(/(file:\/\S+|https?:\/\/[^\s]+\.(?:jpg|jpeg|png|gif|webp|svg|bmp|ico)(?:\?[^\s]*)?)/gi);
211
  return (
212
  <p>
213
+ {parts.filter(Boolean).map((part, index) => {
214
+ if (!part || part.trim() === "") return null;
215
+
216
+ if (part.startsWith("file:/")) {
217
+ return (
218
+ <span
219
+ key={index}
220
+ className="inline-flex w-fit items-center justify-center gap-1 font-mono px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-500/10 text-indigo-500 dark:bg-indigo-500/20 dark:text-indigo-400"
221
+ >
222
+ {getFileIcon(part, "size-2.5")}
223
+ {part.replace("file:/", "")}
224
+ </span>
225
+ );
226
+ } else if (
227
+ part.match(
228
+ /^https?:\/\/[^\s]+\.(?:jpg|jpeg|png|gif|webp|svg|bmp|ico)(?:\?[^\s]*)?$/i
229
+ )
230
+ ) {
231
+ const displayUrl =
232
+ part.length > 35
233
+ ? part.substring(0, 35) + "..."
234
+ : part;
235
+ return (
236
+ <span
237
+ key={index}
238
+ className="inline-flex w-fit items-center justify-center gap-1 font-mono px-2 py-0.5 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400"
239
+ >
240
+ <span className="text-[10px]">🖼️</span>
241
+ {displayUrl}
242
+ </span>
243
+ );
244
+ }
245
+ return part;
246
+ })}
247
  </p>
248
  );
249
  }
lib/providers.ts CHANGED
@@ -30,11 +30,12 @@ export const MODELS = [
30
  companyName: "DeepSeek",
31
  },
32
  {
33
- value: "Qwen/Qwen3-Coder-30B-A3B-Instruct",
34
- label: "Qwen3 Coder 30B A3B Instruct",
35
  providers: ["novita", "hyperbolic"],
36
  logo: QwenLogo,
37
  companyName: "Qwen",
 
38
  },
39
  {
40
  value: "zai-org/GLM-4.7",
 
30
  companyName: "DeepSeek",
31
  },
32
  {
33
+ value: "Qwen/Qwen3-Coder-Next",
34
+ label: "Qwen3 Coder Next",
35
  providers: ["novita", "hyperbolic"],
36
  logo: QwenLogo,
37
  companyName: "Qwen",
38
+ isNew: true,
39
  },
40
  {
41
  value: "zai-org/GLM-4.7",