Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d529c62
refactor(wip): Whole Chat implementation
llanesluis Aug 6, 2025
aed7871
fix: Deps
llanesluis Aug 6, 2025
b645e4d
chore: Update types and create tool to handle theme generation
llanesluis Aug 8, 2025
9a1627d
chore: Add hook and component to handle feedback text
llanesluis Aug 8, 2025
f04f8e8
chore: Check for themeStyles in asssiatant metadata
llanesluis Aug 8, 2025
c8398a7
feat: Improve messages styles and feedback
llanesluis Aug 8, 2025
9ebb2f4
chore: Update react-hook-form deps
llanesluis Aug 8, 2025
fc3e80f
chore: Improve Messages display
llanesluis Aug 9, 2025
eb6a62d
chore: Show a banner when theme is generating
llanesluis Aug 9, 2025
10c1210
chore: Optimize the Messages syncing
llanesluis Aug 9, 2025
d21f6f4
feat: Add debug button to MessageActions in dev mode
llanesluis Aug 9, 2025
9b099ca
chore: messages
llanesluis Aug 12, 2025
7255d70
chore: Improve System prompt + tools + utils
llanesluis Aug 12, 2025
b892b61
fix: correctly display User messages with only images
llanesluis Aug 12, 2025
9c68cae
feat: Handle errors in UI and adapt to AI sdk
llanesluis Aug 14, 2025
57c92e2
feat: Use tool output instead of custom data parts
llanesluis Aug 14, 2025
d9fcca4
chore: System prompt and schema context
llanesluis Aug 14, 2025
6ddf2c0
chore: Gemini 2.5 pro as the default model
llanesluis Aug 14, 2025
df39802
refactor: Change name to Chat Context and apply generated theme autom…
llanesluis Aug 20, 2025
e014b2c
chore: Update ai packages to latest version
llanesluis Aug 20, 2025
b4ee35a
feat: Stop ongoing request before starting a new chat
llanesluis Aug 20, 2025
8c02946
feat: Refactor logic for Scroll start/end sentinels
llanesluis Aug 20, 2025
8a08265
feat: Improve UI streaming with AI Elements and Stream Text utils
llanesluis Aug 20, 2025
e818d31
chore: Generate theme types and logic
llanesluis Aug 26, 2025
5c433ce
feat: Allow customize the speed of streaming text
llanesluis Aug 26, 2025
bc31efb
refactor: Theme generation utils
llanesluis Aug 27, 2025
ae47a08
fix: Avoid duplicated Mention references
llanesluis Aug 28, 2025
5f4ac1a
styles: Improve Chat error banner
llanesluis Aug 28, 2025
76ca58c
feat: Enhance Prompt core implementation
llanesluis Aug 28, 2025
e5fb180
feat: Implement enhance prompt in components
llanesluis Aug 28, 2025
4a4b5b8
chore: Add TODOS comments
llanesluis Aug 28, 2025
3f10d38
chore: Remove Openai provider
llanesluis Aug 29, 2025
dd95ea8
chore: Update AI adk packages
llanesluis Aug 29, 2025
e9b1b58
fix: Free request constant
llanesluis Sep 3, 2025
6c0ccd9
Merge branch 'enhance-ai-chat' into feature/enhance-ai-chat
jnsahaj Sep 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 56 additions & 13 deletions app/ai/components/ai-chat-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@

import { AIChatFormBody } from "@/components/editor/ai/ai-chat-form-body";
import { AlertBanner } from "@/components/editor/ai/alert-banner";
import { EnhancePromptButton } from "@/components/editor/ai/enhance-prompt-button";
import { ImageUploader } from "@/components/editor/ai/image-uploader";
import ThemePresetSelect from "@/components/editor/theme-preset-select";
import { Button } from "@/components/ui/button";
import { useAIChatForm } from "@/hooks/use-ai-chat-form";
import { useAIThemeGenerationCore } from "@/hooks/use-ai-theme-generation-core";
import { useAIEnhancePrompt } from "@/hooks/use-ai-enhance-prompt";
import { useGuards } from "@/hooks/use-guards";
import { MAX_IMAGE_FILES } from "@/lib/constants";
import { cn } from "@/lib/utils";
import { AIPromptData } from "@/types/ai";
import { ArrowUp, Loader, StopCircle } from "lucide-react";

export function AIChatForm({
handleThemeGeneration,
onThemeGeneration,
isGeneratingTheme,
onCancelThemeGeneration,
}: {
handleThemeGeneration: (promptData: AIPromptData | null) => void;
onThemeGeneration: (promptData: AIPromptData) => void;
isGeneratingTheme: boolean;
onCancelThemeGeneration: () => void;
}) {
const { loading: aiGenerateLoading, cancelThemeGeneration } = useAIThemeGenerationCore();
const {
editorContentDraft,
handleContentChange,
Expand All @@ -31,10 +35,23 @@ export function AIChatForm({
handleImageRemove,
isSomeImageUploading,
isUserDragging,
isInitializing,
} = useAIChatForm();

const { checkValidSession, checkValidSubscription } = useGuards();

const { startEnhance, stopEnhance, enhancedPromptAsJsonContent, isEnhancingPrompt } =
useAIEnhancePrompt();

const handleEnhancePrompt = () => {
// TODO: Add subscription check, this should be a Pro only feature
if (!checkValidSession() || !checkValidSubscription()) return; // Act as an early return;

// Only send images that are not loading, and strip loading property
const images = uploadedImages.filter((img) => !img.loading).map(({ url }) => ({ url }));
startEnhance({ ...promptData, images });
};

const handleGenerate = async () => {
if (!checkValidSession() || !checkValidSubscription()) return; // Act as an early return

Expand All @@ -44,7 +61,7 @@ export function AIChatForm({
// Proceed only if there is text, or at least one image
if (isEmptyPrompt && images.length === 0) return;

handleThemeGeneration({
onThemeGeneration({
...promptData,
content: promptData?.content ?? "",
mentions: promptData?.mentions ?? [],
Expand All @@ -61,20 +78,28 @@ export function AIChatForm({
<div className="bg-background relative z-10 flex size-full min-h-[100px] flex-1 flex-col gap-2 overflow-hidden rounded-lg border p-2 shadow-xs">
<AIChatFormBody
isUserDragging={isUserDragging}
aiGenerateLoading={aiGenerateLoading}
disabled={isEnhancingPrompt}
canSubmit={
!isGeneratingTheme &&
!isEnhancingPrompt &&
!isEmptyPrompt &&
!isSomeImageUploading &&
!isInitializing
}
uploadedImages={uploadedImages}
handleImagesUpload={handleImagesUpload}
handleImageRemove={handleImageRemove}
handleContentChange={handleContentChange}
handleGenerate={handleGenerate}
initialEditorContent={editorContentDraft ?? undefined}
textareaKey={editorContentDraft ? "with-draft" : "no-draft"}
externalEditorContent={isEnhancingPrompt ? enhancedPromptAsJsonContent : undefined}
/>

<div className="flex items-center justify-between gap-2">
<div className="flex w-full max-w-64 items-center gap-2 overflow-hidden">
<ThemePresetSelect
disabled={aiGenerateLoading}
disabled={isGeneratingTheme || isEnhancingPrompt || isInitializing}
withCycleThemes={false}
variant="outline"
size="sm"
Expand All @@ -83,22 +108,34 @@ export function AIChatForm({
</div>

<div className="flex items-center gap-2">
{/* TODO: This should be a Pro only feature */}
{promptData?.content ? (
<EnhancePromptButton
isEnhancing={isEnhancingPrompt}
onStart={handleEnhancePrompt}
onStop={stopEnhance}
disabled={isGeneratingTheme || isInitializing}
/>
) : null}

<ImageUploader
fileInputRef={fileInputRef}
onImagesUpload={handleImagesUpload}
onClick={() => fileInputRef.current?.click()}
disabled={
aiGenerateLoading ||
isGeneratingTheme ||
isEnhancingPrompt ||
isInitializing ||
uploadedImages.some((img) => img.loading) ||
uploadedImages.length >= MAX_IMAGE_FILES
}
/>

{aiGenerateLoading ? (
{isGeneratingTheme ? (
<Button
variant="destructive"
size="sm"
onClick={cancelThemeGeneration}
onClick={onCancelThemeGeneration}
className={cn("flex items-center gap-1", "@max-[350px]/form:w-8")}
>
<StopCircle />
Expand All @@ -107,11 +144,17 @@ export function AIChatForm({
) : (
<Button
size="icon"
className="size-8"
className="size-8 shadow-none"
onClick={handleGenerate}
disabled={isEmptyPrompt || isSomeImageUploading || aiGenerateLoading}
disabled={
isEmptyPrompt ||
isSomeImageUploading ||
isGeneratingTheme ||
isEnhancingPrompt ||
isInitializing
}
>
{aiGenerateLoading ? <Loader className="animate-spin" /> : <ArrowUp />}
{isGeneratingTheme ? <Loader className="animate-spin" /> : <ArrowUp />}
</Button>
)}
</div>
Expand Down
29 changes: 18 additions & 11 deletions app/ai/components/ai-chat-hero.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"use client";

import { HorizontalScrollArea } from "@/components/horizontal-scroll-area";
import { useAIGenerateTheme } from "@/hooks/use-ai-generate-theme";
import { useChatContext } from "@/hooks/use-chat-context";
import { useAIThemeGenerationCore } from "@/hooks/use-ai-theme-generation-core";
import { useGuards } from "@/hooks/use-guards";
import { usePostLoginAction } from "@/hooks/use-post-login-action";
import { useAIChatStore } from "@/store/ai-chat-store";
import { usePreferencesStore } from "@/store/preferences-store";
import { AIPromptData } from "@/types/ai";
import { useRouter } from "next/navigation";
Expand All @@ -13,23 +13,23 @@ import { ChatHeading } from "./chat-heading";
import { SuggestedPillActions } from "./suggested-pill-actions";

export function AIChatHero() {
const { generateTheme } = useAIGenerateTheme();
const { startNewChat } = useChatContext();
const { generateThemeCore, isGeneratingTheme, cancelThemeGeneration } =
useAIThemeGenerationCore();
const { checkValidSession, checkValidSubscription } = useGuards();
const router = useRouter();

const { clearMessages } = useAIChatStore();
const { setChatSuggestionsOpen } = usePreferencesStore();

const handleRedirectAndThemeGeneration = async (promptData: AIPromptData | null) => {
const handleRedirectAndThemeGeneration = (promptData: AIPromptData) => {
if (!checkValidSession("signup", "AI_GENERATE_FROM_PAGE", { promptData })) return;
if (!checkValidSubscription()) return;

// Clear the messages and open the chat suggestions when the user starts a chat from the '/ai' page
clearMessages();
startNewChat();
setChatSuggestionsOpen(true);

router.push("/editor/theme?tab=ai");
generateTheme(promptData);
generateThemeCore(promptData);
};

usePostLoginAction("AI_GENERATE_FROM_PAGE", ({ promptData }) => {
Expand All @@ -39,17 +39,24 @@ export function AIChatHero() {
return (
<div className="relative isolate flex w-full flex-1">
<div className="@container relative isolate z-1 mx-auto flex max-w-[49rem] flex-1 flex-col justify-center px-4">
<ChatHeading />
<ChatHeading isGeneratingTheme={isGeneratingTheme} />

{/* Chat form input and suggestions */}
<div className="relative mx-auto flex w-full flex-col gap-2">
<div className="relative isolate z-10 w-full">
<AIChatForm handleThemeGeneration={handleRedirectAndThemeGeneration} />
<AIChatForm
onThemeGeneration={handleRedirectAndThemeGeneration}
isGeneratingTheme={isGeneratingTheme}
onCancelThemeGeneration={cancelThemeGeneration}
/>
</div>

{/* Quick suggestions */}
<HorizontalScrollArea className="mx-auto py-2">
<SuggestedPillActions handleThemeGeneration={handleRedirectAndThemeGeneration} />
<SuggestedPillActions
onThemeGeneration={handleRedirectAndThemeGeneration}
isGeneratingTheme={isGeneratingTheme}
/>
</HorizontalScrollArea>
</div>
</div>
Expand Down
10 changes: 3 additions & 7 deletions app/ai/components/chat-heading.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { useAIThemeGenerationCore } from "@/hooks/use-ai-theme-generation-core";

export function ChatHeading() {
const { loading: aiGenerateLoading } = useAIThemeGenerationCore();

export function ChatHeading({ isGeneratingTheme }: { isGeneratingTheme: boolean }) {
return (
<h1
style={
{
"--gradient-accent": aiGenerateLoading ? "var(--foreground)" : "var(--foreground)",
"--gradient-base": aiGenerateLoading ? "var(--muted-foreground)" : "var(--foreground)",
"--gradient-accent": isGeneratingTheme ? "var(--foreground)" : "var(--foreground)",
"--gradient-base": isGeneratingTheme ? "var(--muted-foreground)" : "var(--foreground)",
} as React.CSSProperties
}
className="animate-text bg-gradient-to-r from-(--gradient-base) via-(--gradient-accent) to-(--gradient-base) bg-[200%_auto] bg-clip-text pb-4 text-center text-[clamp(24px,7cqw,46px)] font-semibold tracking-tighter text-pretty text-transparent"
Expand Down
19 changes: 9 additions & 10 deletions app/ai/components/suggested-pill-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import { PillActionButton } from "@/components/editor/ai/pill-action-button";
import { useAIThemeGenerationCore } from "@/hooks/use-ai-theme-generation-core";
import { useImageUpload } from "@/hooks/use-image-upload";
import { imageUploadReducer } from "@/hooks/use-image-upload-reducer";
import { MAX_IMAGE_FILE_SIZE } from "@/lib/constants";
Expand All @@ -12,12 +11,12 @@ import { ImageIcon, Sparkles } from "lucide-react";
import { useEffect, useReducer } from "react";

export function SuggestedPillActions({
handleThemeGeneration,
onThemeGeneration,
isGeneratingTheme,
}: {
handleThemeGeneration: (promptData: AIPromptData | null) => void;
onThemeGeneration: (promptData: AIPromptData) => void;
isGeneratingTheme: boolean;
}) {
const { loading: aiIsGenerating } = useAIThemeGenerationCore();

const [uploadedImages, dispatch] = useReducer(imageUploadReducer, []);

const { fileInputRef, handleImagesUpload, canUploadMore, isSomeImageUploading } = useImageUpload({
Expand All @@ -30,7 +29,7 @@ export function SuggestedPillActions({
// Automatically send prompt when an image is selected and loaded
useEffect(() => {
if (uploadedImages.length > 0 && !isSomeImageUploading) {
handleThemeGeneration({
onThemeGeneration({
content: "", // No text prompt
mentions: [], // No mentions
images: [uploadedImages[0]],
Expand All @@ -42,7 +41,7 @@ export function SuggestedPillActions({

const handleSetPrompt = async (prompt: string) => {
const promptData = createCurrentThemePrompt({ prompt });
handleThemeGeneration(promptData);
onThemeGeneration(promptData);
};

const handleImageButtonClick = () => {
Expand All @@ -61,14 +60,14 @@ export function SuggestedPillActions({

return (
<>
<PillActionButton onClick={handleImageButtonClick} disabled={aiIsGenerating}>
<PillActionButton onClick={handleImageButtonClick} disabled={isGeneratingTheme}>
<input
type="file"
accept="image/*"
multiple={false}
ref={fileInputRef}
onChange={handleImageUpload}
disabled={aiIsGenerating}
disabled={isGeneratingTheme}
style={{ display: "none" }}
Comment on lines +63 to 71
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Disable image controls while uploading or when max reached.

Also gate the hidden input to prevent concurrent uploads.

-      <PillActionButton onClick={handleImageButtonClick} disabled={isGeneratingTheme}>
+      <PillActionButton
+        onClick={handleImageButtonClick}
+        disabled={isGeneratingTheme || isSomeImageUploading || !canUploadMore}
+      >
@@
           ref={fileInputRef}
           onChange={handleImageUpload}
-          disabled={isGeneratingTheme}
+          disabled={isGeneratingTheme || isSomeImageUploading || !canUploadMore}
           style={{ display: "none" }}
         />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PillActionButton onClick={handleImageButtonClick} disabled={isGeneratingTheme}>
<input
type="file"
accept="image/*"
multiple={false}
ref={fileInputRef}
onChange={handleImageUpload}
disabled={aiIsGenerating}
disabled={isGeneratingTheme}
style={{ display: "none" }}
<PillActionButton
onClick={handleImageButtonClick}
disabled={isGeneratingTheme || isSomeImageUploading || !canUploadMore}
>
<input
type="file"
accept="image/*"
multiple={false}
ref={fileInputRef}
onChange={handleImageUpload}
disabled={isGeneratingTheme || isSomeImageUploading || !canUploadMore}
style={{ display: "none" }}
🤖 Prompt for AI Agents
In app/ai/components/suggested-pill-actions.tsx around lines 63 to 71, the
hidden file input and image upload button aren't fully gated: update the logic
so the PillActionButton and the hidden <input> are disabled when
isGeneratingTheme is true OR when the image upload count has reached the
configured max (e.g., disableWhenMaxReached flag), and prevent the input's
onChange handler from starting a new upload if an upload is already in progress
(return early if isGeneratingTheme or max reached). Ensure the ref click only
triggers when allowed and the input's disabled prop is set accordingly to
prevent concurrent uploads.

/>
<ImageIcon /> From an Image
Expand All @@ -78,7 +77,7 @@ export function SuggestedPillActions({
<PillActionButton
key={key}
onClick={() => handleSetPrompt(prompt)}
disabled={aiIsGenerating}
disabled={isGeneratingTheme}
>
<Sparkles /> {label}
</PillActionButton>
Expand Down
1 change: 0 additions & 1 deletion app/ai/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Metadata } from "next";
import { AIAnnouncement } from "./components/ai-announcement";
import { AIChatHero } from "./components/ai-chat-hero";

Expand Down
68 changes: 68 additions & 0 deletions app/api/enhance-prompt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { AIPromptData } from "@/types/ai";
import { buildUserContentPartsFromPromptData } from "@/utils/ai/message-converter";
import { baseProviderOptions, MODELS } from "@/utils/ai/providers";
import { smoothStream, streamText } from "ai";

export async function POST(req: Request) {
// TODO: Add session and subscription check, this should be a Pro only feature
// TODO: Record AI usage, providing the model id to `recordAIUsage` function
Comment on lines +7 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enhance-prompt API route is missing critical security checks and usage tracking. It currently allows unrestricted access to the AI prompt enhancement feature without session validation or subscription verification.

View Details

Analysis

The TODO comments on lines 7-8 indicate that this API endpoint is missing essential security mechanisms:

  1. Session and subscription check: The endpoint should verify that the user is authenticated and has a valid subscription before allowing access to this Pro-only feature. Without these checks, any user (including unauthenticated ones) can make requests to this endpoint.

  2. AI usage tracking: The endpoint should record AI usage via the recordAIUsage function to track token consumption and ensure proper billing/limits are enforced.

This creates a security vulnerability where:

  • Unauthenticated users can consume AI resources
  • Usage isn't tracked, potentially leading to unexpected costs
  • Pro-only feature restrictions are bypassed

The implementation should add the same validation pattern used in the generate-theme route, including user authentication, subscription validation, and usage recording.


const body = await req.json();
const { prompt: _prompt, promptData }: { prompt: string; promptData: AIPromptData } = body;
const userContentParts = buildUserContentPartsFromPromptData(promptData);

Comment on lines +10 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate request body and fail fast with 400 on invalid input.
Prevents obscure errors in buildUserContentPartsFromPromptData.

-  const body = await req.json();
-  const { prompt: _prompt, promptData }: { prompt: string; promptData: AIPromptData } = body;
+  const body = await req.json();
+  const { promptData }: { prompt: string; promptData: AIPromptData } = body ?? {};
+  if (!promptData || typeof promptData.content !== "string") {
+    return new Response(JSON.stringify({ error: "Invalid body: promptData.content is required" }), {
+      status: 400,
+      headers: { "content-type": "application/json" },
+    });
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const body = await req.json();
const { prompt: _prompt, promptData }: { prompt: string; promptData: AIPromptData } = body;
const userContentParts = buildUserContentPartsFromPromptData(promptData);
const body = await req.json();
const { promptData }: { prompt: string; promptData: AIPromptData } = body ?? {};
if (!promptData || typeof promptData.content !== "string") {
return new Response(JSON.stringify({ error: "Invalid body: promptData.content is required" }), {
status: 400,
headers: { "content-type": "application/json" },
});
}
const userContentParts = buildUserContentPartsFromPromptData(promptData);
🤖 Prompt for AI Agents
In app/api/enhance-prompt/route.ts around lines 10 to 13, the handler currently
reads req.json() and immediately destructures prompt and promptData, which can
cause obscure errors inside buildUserContentPartsFromPromptData when the request
body is missing or malformed; add explicit validation after parsing body to
verify that body is an object, that prompt is a non-empty string and promptData
exists and matches expected shape (or at minimum is an object), and if
validation fails return an HTTP 400 response with a clear error message; perform
this check before calling buildUserContentPartsFromPromptData so the route fails
fast on invalid input.

const result = streamText({
system: ENHANCE_PROMPT_SYSTEM,
messages: [
{
role: "user",
content: userContentParts,
},
],
model: MODELS.promptEnhancement,
providerOptions: baseProviderOptions,
experimental_transform: smoothStream({
delayInMs: 10,
chunking: "word",
}),
});

return result.toUIMessageStreamResponse();
}

const ENHANCE_PROMPT_SYSTEM = `# Role
You are a prompt refinement specialist for a shadcn/ui theme generator. Rewrite the user's input into a precise, ready-to-use prompt that preserves the original intent, language, and tone. Write as the requester of the theme, not as an assistant.
# Core principles
- Language matching: respond in the exact same language as the user
- Cultural context: respect regional expressions, slang, and cultural references
- Length limit: output must NOT exceed 500 characters
- If when analyzing the user's prompt you recognize a vibe, style, or visual reference, include it in the output.
# Mentions
- User input may include mentions like @Current Theme or @PresetName. Mentions are always in the format of @[label].
- Mentions are predefined styles that are intended to be used as the base or reference for the theme.
- Preserve them verbatim in the output text (same labels, same order if possible).
- Do not invent new mentions. Only keep and reposition mentions that appear in the user's prompt or in the provided mention list.
- Avoid repeating the same mention multiple times.
# Enhancement guidelines
- If the prompt is vague, add concrete visual details (colors, mood, typography, style references)
- If it mentions a brand or style, include relevant design characteristics
- If it's already detailed, clarify ambiguous parts and tighten wording
- Preserve any specific requests (colors, fonts, mood, etc.)
- Add context that helps the theme generator understand the desired aesthetic
# Output rules
- Output a single line of plain text suitable to paste into the Generate Theme tool
- No greetings or meta commentary; do not address the user
- Do not narrate with phrases like "I'm seeing", "let's", "alright", or "what you want is"
- No bullets, no quotes, no markdown, no JSON
# What NOT to do
- Don't change the user's core request
- Don't add conflicting style directions
- Don't exceed 500 characters
- Don't use a different language than the user
- Do not list the mentions' properties in the enhanced prompt.
- Do not use em dashes (—)`;
Loading