- Create a React component with increment and decrement buttons that updates a counter value.
+ {challenge.question}
Your challenge timer will start right after the first prompt
diff --git a/app/lib/challenges.ts b/app/lib/challenges.ts
new file mode 100644
index 0000000000..efeb2ee21c
--- /dev/null
+++ b/app/lib/challenges.ts
@@ -0,0 +1,32 @@
+export interface Challenge {
+ id: string;
+ title: string;
+ question: string;
+}
+
+let challengesCache: Challenge[] | null = null;
+
+async function loadChallenges(): Promise {
+ if (challengesCache) {
+ return challengesCache;
+ }
+
+ try {
+ // In production, we'll need to handle file loading differently for Cloudflare Workers
+ const challengesModule = await import('../../data/challenges.json');
+ challengesCache = challengesModule.default as Challenge[];
+ return challengesCache;
+ } catch (error) {
+ console.error('Failed to load challenges:', error);
+ return [];
+ }
+}
+
+export async function getChallengeById(id: string): Promise {
+ const challenges = await loadChallenges();
+ return challenges.find(challenge => challenge.id === id) || null;
+}
+
+export async function getAllChallenges(): Promise {
+ return loadChallenges();
+}
\ No newline at end of file
diff --git a/app/routes/challenge.$id.tsx b/app/routes/challenge.$id.tsx
index 91ff4d4361..f54070a016 100644
--- a/app/routes/challenge.$id.tsx
+++ b/app/routes/challenge.$id.tsx
@@ -1,25 +1,41 @@
-import { json, type MetaFunction } from '@remix-run/cloudflare';
-import { useParams } from '@remix-run/react';
+import { json, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/cloudflare';
+import { useLoaderData } from '@remix-run/react';
import { ClientOnly } from 'remix-utils/client-only';
import { ChallengeChat as ChallengeChatFallback } from '~/components/chat/ChallengeChat';
import { ChallengeChatClient } from '~/components/chat/ChallengeChat.client';
import { Header } from '~/components/header/Header';
+import { getChallengeById, type Challenge } from '~/lib/challenges';
-export const meta: MetaFunction = () => {
- return [{ title: 'Bolt - Challenge' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
+export const meta: MetaFunction = ({ data }) => {
+ const title = data?.challenge ? `${data.challenge.title} - Challenge` : 'Challenge Not Found';
+ return [{ title }, { name: 'description', content: 'Code challenges powered by AI' }];
};
-export const loader = () => json({});
+export async function loader({ params }: LoaderFunctionArgs) {
+ const { id } = params;
-export default function Challenge() {
- const { id } = useParams();
+ if (!id) {
+ throw new Response('Challenge ID is required', { status: 400 });
+ }
+
+ const challenge = await getChallengeById(id);
+
+ if (!challenge) {
+ throw new Response('Challenge not found', { status: 404 });
+ }
- console.log('Challenge ID:', id);
+ return json({ challenge });
+}
+
+export default function Challenge() {
+ const { challenge } = useLoaderData();
return (
- }>{() => }
+ }>
+ {() => }
+
);
}
\ No newline at end of file
diff --git a/data/challenges.json b/data/challenges.json
new file mode 100644
index 0000000000..99d82fc03d
--- /dev/null
+++ b/data/challenges.json
@@ -0,0 +1,12 @@
+[
+ {
+ "id": "counter",
+ "title": "Build a Counter",
+ "question": "Make a click counter with +, -, and reset buttons. The value must never go below 0."
+ },
+ {
+ "id": "todo",
+ "title": "Todo List",
+ "question": "Build a todo list with add and delete functionality."
+ }
+]
\ No newline at end of file
From a2d13a18d31e7ae5dc4a3785f997ebf764346081 Mon Sep 17 00:00:00 2001
From: Jad El Asmar <42979241+1elasmarjad@users.noreply.github.com>
Date: Sat, 13 Sep 2025 14:29:09 -0400
Subject: [PATCH 10/53] Update ChallengeChat.tsx
---
app/components/chat/ChallengeChat.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/components/chat/ChallengeChat.tsx b/app/components/chat/ChallengeChat.tsx
index 220db09943..1972d17799 100644
--- a/app/components/chat/ChallengeChat.tsx
+++ b/app/components/chat/ChallengeChat.tsx
@@ -130,7 +130,7 @@ export const ChallengeChat = React.forwardRef
From c3962d79054e4ce72048dd5a77d1434422c122be Mon Sep 17 00:00:00 2001
From: Jad El Asmar <42979241+1elasmarjad@users.noreply.github.com>
Date: Sat, 13 Sep 2025 14:58:14 -0400
Subject: [PATCH 11/53] Added challenge reference to chats + removed sidebar
---
app/components/chat/BaseChat.tsx | 2 -
app/components/chat/ChallengeChat.client.tsx | 6 ++
app/components/chat/Chat.client.tsx | 15 +++--
app/lib/challengeSession.ts | 59 ++++++++++++++++++++
app/lib/persistence/db.ts | 23 ++++++++
app/lib/persistence/useChatHistory.ts | 39 +++++++++++--
6 files changed, 134 insertions(+), 10 deletions(-)
create mode 100644 app/lib/challengeSession.ts
diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx
index c4f90f43a1..c67609cb32 100644
--- a/app/components/chat/BaseChat.tsx
+++ b/app/components/chat/BaseChat.tsx
@@ -1,7 +1,6 @@
import type { Message } from 'ai';
import React, { type RefCallback } from 'react';
import { ClientOnly } from 'remix-utils/client-only';
-import { Menu } from '~/components/sidebar/Menu.client';
import { IconButton } from '~/components/ui/IconButton';
import { Workbench } from '~/components/workbench/Workbench.client';
import { classNames } from '~/utils/classNames';
@@ -68,7 +67,6 @@ export const BaseChat = React.forwardRef(
)}
data-chat-visible={showChat}
>
- {() => }
{!chatStarted && (
diff --git a/app/components/chat/ChallengeChat.client.tsx b/app/components/chat/ChallengeChat.client.tsx
index 54311ec0af..7f03ca33f0 100644
--- a/app/components/chat/ChallengeChat.client.tsx
+++ b/app/components/chat/ChallengeChat.client.tsx
@@ -9,6 +9,7 @@ import { useChatHistory } from '~/lib/persistence';
import { chatStore } from '~/lib/stores/chat';
import { workbenchStore } from '~/lib/stores/workbench';
import { type Challenge } from '~/lib/challenges';
+import { setChallengeContext } from '~/lib/challengeSession';
import { fileModificationsToHTML } from '~/utils/diff';
import { cubicEasingFn } from '~/utils/easings';
import { createScopedLogger, renderLogger } from '~/utils/logger';
@@ -164,6 +165,11 @@ export const ChallengeChatImpl = memo(({ challenge, initialMessages, storeMessag
return;
}
+ // Store challenge context when first message is sent
+ if (messages.length === 0) {
+ setChallengeContext(challenge.id, challenge);
+ }
+
/**
* @note (delm) Usually saving files shouldn't take long but it may take longer if there
* many unsaved files. In that case we need to block user input and show an indicator
diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx
index dff7598e40..9777406b21 100644
--- a/app/components/chat/Chat.client.tsx
+++ b/app/components/chat/Chat.client.tsx
@@ -5,7 +5,7 @@ import { useAnimate } from 'framer-motion';
import { memo, useEffect, useRef, useState } from 'react';
import { cssTransition, toast, ToastContainer } from 'react-toastify';
import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
-import { useChatHistory } from '~/lib/persistence';
+import { useChatHistory, type ChatHistoryItem } from '~/lib/persistence';
import { chatStore } from '~/lib/stores/chat';
import { workbenchStore } from '~/lib/stores/workbench';
import { fileModificationsToHTML } from '~/utils/diff';
@@ -23,11 +23,17 @@ const logger = createScopedLogger('Chat');
export function Chat() {
renderLogger.trace('Chat');
- const { ready, initialMessages, storeMessageHistory } = useChatHistory();
+ const { ready, initialMessages, storeMessageHistory, chatData } = useChatHistory();
return (
<>
- {ready &&
}
+ {ready && (
+
+ )}
{
return (
@@ -62,9 +68,10 @@ export function Chat() {
interface ChatProps {
initialMessages: Message[];
storeMessageHistory: (messages: Message[]) => Promise;
+ chatData?: ChatHistoryItem;
}
-export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProps) => {
+export const ChatImpl = memo(({ initialMessages, storeMessageHistory, chatData }: ChatProps) => {
useShortcuts();
const textareaRef = useRef(null);
diff --git a/app/lib/challengeSession.ts b/app/lib/challengeSession.ts
new file mode 100644
index 0000000000..dcaacd43ce
--- /dev/null
+++ b/app/lib/challengeSession.ts
@@ -0,0 +1,59 @@
+import { type Challenge } from './challenges';
+
+interface ChallengeSession {
+ challengeId: string;
+ challenge: Challenge;
+ timestamp: number;
+}
+
+const CHALLENGE_SESSION_KEY = 'bolt-challenge-context';
+
+export function setChallengeContext(challengeId: string, challenge: Challenge): void {
+ try {
+ const sessionData: ChallengeSession = {
+ challengeId,
+ challenge,
+ timestamp: Date.now(),
+ };
+
+ sessionStorage.setItem(CHALLENGE_SESSION_KEY, JSON.stringify(sessionData));
+ } catch (error) {
+ console.warn('Failed to store challenge context in session storage:', error);
+ }
+}
+
+export function getChallengeContext(): ChallengeSession | null {
+ try {
+ const stored = sessionStorage.getItem(CHALLENGE_SESSION_KEY);
+ if (!stored) {
+ return null;
+ }
+
+ const sessionData: ChallengeSession = JSON.parse(stored);
+
+ // Check if the session is not too old (1 hour max)
+ const maxAge = 60 * 60 * 1000; // 1 hour in milliseconds
+ if (Date.now() - sessionData.timestamp > maxAge) {
+ clearChallengeContext();
+ return null;
+ }
+
+ return sessionData;
+ } catch (error) {
+ console.warn('Failed to retrieve challenge context from session storage:', error);
+ clearChallengeContext();
+ return null;
+ }
+}
+
+export function clearChallengeContext(): void {
+ try {
+ sessionStorage.removeItem(CHALLENGE_SESSION_KEY);
+ } catch (error) {
+ console.warn('Failed to clear challenge context from session storage:', error);
+ }
+}
+
+export function hasChallengeContext(): boolean {
+ return getChallengeContext() !== null;
+}
\ No newline at end of file
diff --git a/app/lib/persistence/db.ts b/app/lib/persistence/db.ts
index 7a952e3441..46e0bfb57b 100644
--- a/app/lib/persistence/db.ts
+++ b/app/lib/persistence/db.ts
@@ -47,6 +47,8 @@ export async function setMessages(
messages: Message[],
urlId?: string,
description?: string,
+ challengeId?: string,
+ challengeData?: any,
): Promise {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readwrite');
@@ -58,6 +60,8 @@ export async function setMessages(
urlId,
description,
timestamp: new Date().toISOString(),
+ challengeId,
+ challengeData,
});
request.onsuccess = () => resolve();
@@ -134,6 +138,25 @@ export async function getUrlId(db: IDBDatabase, id: string): Promise {
}
}
+export async function getChallengeNextId(db: IDBDatabase, challengeId: string): Promise {
+ const idList = await getUrlIds(db);
+ const challengePrefix = `${challengeId}-v`;
+
+ // Find all existing challenge-based IDs
+ const existingVersions = idList
+ .filter(id => id && id.startsWith(challengePrefix))
+ .map(id => {
+ const versionMatch = id.match(new RegExp(`^${challengeId}-v(\\d+)$`));
+ return versionMatch ? parseInt(versionMatch[1], 10) : 0;
+ })
+ .filter(version => version > 0);
+
+ // Find the next version number
+ const nextVersion = existingVersions.length === 0 ? 1 : Math.max(...existingVersions) + 1;
+
+ return `${challengeId}-v${nextVersion}`;
+}
+
async function getUrlIds(db: IDBDatabase): Promise {
return new Promise((resolve, reject) => {
const transaction = db.transaction('chats', 'readonly');
diff --git a/app/lib/persistence/useChatHistory.ts b/app/lib/persistence/useChatHistory.ts
index e56275327e..36baa0c76b 100644
--- a/app/lib/persistence/useChatHistory.ts
+++ b/app/lib/persistence/useChatHistory.ts
@@ -4,7 +4,9 @@ import { atom } from 'nanostores';
import type { Message } from 'ai';
import { toast } from 'react-toastify';
import { workbenchStore } from '~/lib/stores/workbench';
-import { getMessages, getNextId, getUrlId, openDatabase, setMessages } from './db';
+import { type Challenge } from '~/lib/challenges';
+import { getChallengeContext, clearChallengeContext } from '~/lib/challengeSession';
+import { getMessages, getNextId, getUrlId, getChallengeNextId, openDatabase, setMessages } from './db';
export interface ChatHistoryItem {
id: string;
@@ -12,6 +14,8 @@ export interface ChatHistoryItem {
description?: string;
messages: Message[];
timestamp: string;
+ challengeId?: string;
+ challengeData?: Challenge;
}
const persistenceEnabled = !import.meta.env.VITE_DISABLE_PERSISTENCE;
@@ -28,6 +32,7 @@ export function useChatHistory() {
const [initialMessages, setInitialMessages] = useState([]);
const [ready, setReady] = useState(false);
const [urlId, setUrlId] = useState();
+ const [chatData, setChatData] = useState();
useEffect(() => {
if (!db) {
@@ -46,6 +51,7 @@ export function useChatHistory() {
if (storedMessages && storedMessages.messages.length > 0) {
setInitialMessages(storedMessages.messages);
setUrlId(storedMessages.urlId);
+ setChatData(storedMessages);
description.set(storedMessages.description);
chatId.set(storedMessages.id);
} else {
@@ -63,12 +69,14 @@ export function useChatHistory() {
return {
ready: !mixedId || ready,
initialMessages,
+ chatData,
storeMessageHistory: async (messages: Message[]) => {
if (!db || messages.length === 0) {
return;
}
const { firstArtifact } = workbenchStore;
+ const challengeContext = getChallengeContext();
if (!urlId && firstArtifact?.id) {
const urlId = await getUrlId(db, firstArtifact.id);
@@ -82,16 +90,39 @@ export function useChatHistory() {
}
if (initialMessages.length === 0 && !chatId.get()) {
- const nextId = await getNextId(db);
+ let nextId: string;
+ let generatedUrlId: string;
+
+ if (challengeContext) {
+ // Generate challenge-based ID
+ generatedUrlId = await getChallengeNextId(db, challengeContext.challengeId);
+ nextId = await getNextId(db);
+
+ // Clear challenge context after first use
+ clearChallengeContext();
+ } else {
+ // Regular ID generation
+ nextId = await getNextId(db);
+ generatedUrlId = urlId || nextId;
+ }
chatId.set(nextId);
if (!urlId) {
- navigateChat(nextId);
+ navigateChat(generatedUrlId);
+ setUrlId(generatedUrlId);
}
}
- await setMessages(db, chatId.get() as string, messages, urlId, description.get());
+ await setMessages(
+ db,
+ chatId.get() as string,
+ messages,
+ urlId,
+ description.get(),
+ challengeContext?.challengeId,
+ challengeContext?.challenge
+ );
},
};
}
From cac0d9d60374d7fb4ecfdeecbb8dd3f96a012dd7 Mon Sep 17 00:00:00 2001
From: Jad El Asmar <42979241+1elasmarjad@users.noreply.github.com>
Date: Sat, 13 Sep 2025 15:01:45 -0400
Subject: [PATCH 12/53] title shows up as question
---
app/lib/persistence/ChatDescription.client.tsx | 8 ++++++--
app/lib/persistence/useChatHistory.ts | 8 ++++++--
2 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/app/lib/persistence/ChatDescription.client.tsx b/app/lib/persistence/ChatDescription.client.tsx
index 9c7cbf225e..09175d6b84 100644
--- a/app/lib/persistence/ChatDescription.client.tsx
+++ b/app/lib/persistence/ChatDescription.client.tsx
@@ -1,6 +1,10 @@
import { useStore } from '@nanostores/react';
-import { description } from './useChatHistory';
+import { description, useChatHistory } from './useChatHistory';
export function ChatDescription() {
- return useStore(description);
+ const regularDescription = useStore(description);
+ const { chatData } = useChatHistory();
+
+ // Priority: Challenge title > Regular description
+ return chatData?.challengeData?.title || regularDescription;
}
diff --git a/app/lib/persistence/useChatHistory.ts b/app/lib/persistence/useChatHistory.ts
index 36baa0c76b..3683bc3809 100644
--- a/app/lib/persistence/useChatHistory.ts
+++ b/app/lib/persistence/useChatHistory.ts
@@ -85,8 +85,12 @@ export function useChatHistory() {
setUrlId(urlId);
}
- if (!description.get() && firstArtifact?.title) {
- description.set(firstArtifact?.title);
+ if (!description.get()) {
+ if (challengeContext?.challenge?.title) {
+ description.set(challengeContext.challenge.title);
+ } else if (firstArtifact?.title) {
+ description.set(firstArtifact?.title);
+ }
}
if (initialMessages.length === 0 && !chatId.get()) {
From 2c17ca1ab9595798f9704d82e881a2a0c9700601 Mon Sep 17 00:00:00 2001
From: azamjb
Date: Sat, 13 Sep 2025 15:07:09 -0400
Subject: [PATCH 13/53] navbar and profile page
---
app/components/header/Header.tsx | 25 +++-
app/routes/profile.tsx | 211 +++++++++++++++++++++++++++++++
2 files changed, 235 insertions(+), 1 deletion(-)
create mode 100644 app/routes/profile.tsx
diff --git a/app/components/header/Header.tsx b/app/components/header/Header.tsx
index 0199fc3555..9d0f70c2a2 100644
--- a/app/components/header/Header.tsx
+++ b/app/components/header/Header.tsx
@@ -4,20 +4,43 @@ import { chatStore } from '~/lib/stores/chat';
import { classNames } from '~/utils/classNames';
import { HeaderActionButtons } from './HeaderActionButtons.client';
import { ChatDescription } from '~/lib/persistence/ChatDescription.client';
+import { Link, useLocation } from '@remix-run/react';
export function Header() {
const chat = useStore(chatStore);
+ const location = useLocation();
return (
{/* Ghost Element */}
diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx
index 9777406b21..e6cd46ce91 100644
--- a/app/components/chat/Chat.client.tsx
+++ b/app/components/chat/Chat.client.tsx
@@ -4,7 +4,7 @@ import { useChat } from 'ai/react';
import { useAnimate } from 'framer-motion';
import { memo, useEffect, useRef, useState } from 'react';
import { cssTransition, toast, ToastContainer } from 'react-toastify';
-import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
+import { useMessageParser, useShortcuts, useSnapScroll } from '~/lib/hooks';
import { useChatHistory, type ChatHistoryItem } from '~/lib/persistence';
import { chatStore } from '~/lib/stores/chat';
import { workbenchStore } from '~/lib/stores/workbench';
@@ -94,7 +94,6 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, chatData }
initialMessages,
});
- const { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer } = usePromptEnhancer();
const { parsedMessages, parseMessages } = useMessageParser();
const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
@@ -198,8 +197,6 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, chatData }
setInput('');
- resetEnhancer();
-
textareaRef.current?.blur();
};
@@ -213,8 +210,6 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, chatData }
showChat={showChat}
chatStarted={chatStarted}
isStreaming={isLoading}
- enhancingPrompt={enhancingPrompt}
- promptEnhanced={promptEnhanced}
sendMessage={sendMessage}
messageRef={messageRef}
scrollRef={scrollRef}
@@ -230,12 +225,6 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory, chatData }
content: parsedMessages[i] || '',
};
})}
- enhancePrompt={() => {
- enhancePrompt(input, (input) => {
- setInput(input);
- scrollTextArea();
- });
- }}
/>
);
});
From e7f0a3cecc5e9e7b21d40de6744f131acc56fd93 Mon Sep 17 00:00:00 2001
From: Jad El Asmar <42979241+1elasmarjad@users.noreply.github.com>
Date: Sat, 13 Sep 2025 16:59:16 -0400
Subject: [PATCH 17/53] Added gemini 2.5 flash
---
.claude/settings.local.json | 15 ++
app/lib/.server/llm/stream-text.ts | 10 +-
app/routes/api.chat.ts | 4 +-
app/routes/api.enhancer.ts | 60 -----
package.json | 4 +-
pnpm-lock.yaml | 386 ++++-------------------------
6 files changed, 67 insertions(+), 412 deletions(-)
create mode 100644 .claude/settings.local.json
delete mode 100644 app/routes/api.enhancer.ts
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000000..898d863179
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,15 @@
+{
+ "permissions": {
+ "allow": [
+ "mcp__ide__getDiagnostics",
+ "WebSearch",
+ "Bash(pnpm update:*)",
+ "Bash(pnpm typecheck:*)",
+ "WebFetch(domain:ai-sdk.dev)",
+ "Bash(pnpm list:*)",
+ "WebFetch(domain:www.npmjs.com)"
+ ],
+ "deny": [],
+ "ask": []
+ }
+}
\ No newline at end of file
diff --git a/app/lib/.server/llm/stream-text.ts b/app/lib/.server/llm/stream-text.ts
index 63b2845455..0792336023 100644
--- a/app/lib/.server/llm/stream-text.ts
+++ b/app/lib/.server/llm/stream-text.ts
@@ -1,6 +1,6 @@
import { streamText as _streamText, convertToCoreMessages } from 'ai';
import { getSystemPrompt } from './prompts';
-import { anthropic } from '@ai-sdk/anthropic';
+import { google } from "@ai-sdk/google"
interface ToolResult {
toolCallId: string;
@@ -20,11 +20,13 @@ export type Messages = Message[];
export type StreamingOptions = Omit[0], 'model'>;
-export function streamText(messages: Messages, env: Env, options?: StreamingOptions) {
+// Use anthropic directly
+
+export function streamText(messages: Messages, _env: Env, options?: StreamingOptions) {
return _streamText({
- model: anthropic("claude-sonnet-4-20250514"),
+ model: google("gemini-2.5-flash"),
system: getSystemPrompt(),
- messages: convertToCoreMessages(messages),
+ messages: convertToCoreMessages(messages),
...options,
});
}
diff --git a/app/routes/api.chat.ts b/app/routes/api.chat.ts
index b685ac853a..df96972621 100644
--- a/app/routes/api.chat.ts
+++ b/app/routes/api.chat.ts
@@ -34,13 +34,13 @@ async function chatAction({ context, request }: ActionFunctionArgs) {
const result = await streamText(messages, context.cloudflare.env, options);
- return stream.switchSource(result.toAIStream());
+ return stream.switchSource(result.toDataStream());
},
};
const result = await streamText(messages, context.cloudflare.env, options);
- stream.switchSource(result.toAIStream());
+ stream.switchSource(result.toDataStream());
return new Response(stream.readable, {
status: 200,
diff --git a/app/routes/api.enhancer.ts b/app/routes/api.enhancer.ts
deleted file mode 100644
index 5c8175ca30..0000000000
--- a/app/routes/api.enhancer.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { type ActionFunctionArgs } from '@remix-run/cloudflare';
-import { StreamingTextResponse, parseStreamPart } from 'ai';
-import { streamText } from '~/lib/.server/llm/stream-text';
-import { stripIndents } from '~/utils/stripIndent';
-
-const encoder = new TextEncoder();
-const decoder = new TextDecoder();
-
-export async function action(args: ActionFunctionArgs) {
- return enhancerAction(args);
-}
-
-async function enhancerAction({ context, request }: ActionFunctionArgs) {
- const { message } = await request.json<{ message: string }>();
-
- try {
- const result = await streamText(
- [
- {
- role: 'user',
- content: stripIndents`
- I want you to improve the user prompt that is wrapped in \`\` tags.
-
- IMPORTANT: Only respond with the improved prompt and nothing else!
-
-
- ${message}
-
- `,
- },
- ],
- context.cloudflare.env,
- );
-
- const transformStream = new TransformStream({
- transform(chunk, controller) {
- const processedChunk = decoder
- .decode(chunk)
- .split('\n')
- .filter((line) => line !== '')
- .map(parseStreamPart)
- .map((part) => part.value)
- .join('');
-
- controller.enqueue(encoder.encode(processedChunk));
- },
- });
-
- const transformedStream = result.toAIStream().pipeThrough(transformStream);
-
- return new StreamingTextResponse(transformedStream);
- } catch (error) {
- console.log(error);
-
- throw new Response(null, {
- status: 500,
- statusText: 'Internal Server Error',
- });
- }
-}
diff --git a/package.json b/package.json
index ea7228daf1..9c8457faa5 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
},
"dependencies": {
"@ai-sdk/anthropic": "^0.0.39",
- "@ai-sdk/google": "^2.0.14",
+ "@ai-sdk/google": "^1.2.22",
"@codemirror/autocomplete": "^6.18.7",
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-cpp": "^6.0.3",
@@ -55,7 +55,7 @@
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
- "ai": "^3.4.33",
+ "ai": "^4.3.19",
"date-fns": "^3.6.0",
"diff": "^5.2.0",
"framer-motion": "^11.18.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 994448516d..e27237d04c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -15,8 +15,8 @@ importers:
specifier: ^0.0.39
version: 0.0.39(zod@3.25.76)
'@ai-sdk/google':
- specifier: ^2.0.14
- version: 2.0.14(zod@3.25.76)
+ specifier: ^1.2.22
+ version: 1.2.22(zod@3.25.76)
'@codemirror/autocomplete':
specifier: ^6.18.7
version: 6.18.7
@@ -108,8 +108,8 @@ importers:
specifier: ^5.5.0
version: 5.5.0
ai:
- specifier: ^3.4.33
- version: 3.4.33(react@18.3.1)(sswr@2.2.0(svelte@4.2.18))(svelte@4.2.18)(vue@3.4.30(typescript@5.9.2))(zod@3.25.76)
+ specifier: ^4.3.19
+ version: 4.3.19(react@18.3.1)(zod@3.25.76)
date-fns:
specifier: ^3.6.0
version: 3.6.0
@@ -240,20 +240,11 @@ packages:
peerDependencies:
zod: ^3.0.0
- '@ai-sdk/google@2.0.14':
- resolution: {integrity: sha512-OCBBkEUq1RNLkbJuD+ejqGsWDD0M5nRyuFWDchwylxy0J4HSsAiGNhutNYVTdnqmNw+r9LyZlkyZ1P4YfAfLdg==}
- engines: {node: '>=18'}
- peerDependencies:
- zod: ^3.25.76 || ^4
-
- '@ai-sdk/provider-utils@1.0.22':
- resolution: {integrity: sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==}
+ '@ai-sdk/google@1.2.22':
+ resolution: {integrity: sha512-Ppxu3DIieF1G9pyQ5O1Z646GYR0gkC57YdBqXJ82qvCdhEhZHu0TWhmnOoeIWe2olSbuDeoOY+MfJrW8dzS3Hw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
- peerDependenciesMeta:
- zod:
- optional: true
'@ai-sdk/provider-utils@1.0.9':
resolution: {integrity: sha512-yfdanjUiCJbtGoRGXrcrmXn0pTyDfRIeY6ozDG96D66f2wupZaZvAgKptUa3zDYXtUCQQvcNJ+tipBBfQD/UYA==}
@@ -264,71 +255,35 @@ packages:
zod:
optional: true
- '@ai-sdk/provider-utils@3.0.9':
- resolution: {integrity: sha512-Pm571x5efqaI4hf9yW4KsVlDBDme8++UepZRnq+kqVBWWjgvGhQlzU8glaFq0YJEB9kkxZHbRRyVeHoV2sRYaQ==}
+ '@ai-sdk/provider-utils@2.2.8':
+ resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==}
engines: {node: '>=18'}
peerDependencies:
- zod: ^3.25.76 || ^4
+ zod: ^3.23.8
'@ai-sdk/provider@0.0.17':
resolution: {integrity: sha512-f9j+P5yYRkqKFHxvWae5FI0j6nqROPCoPnMkpc2hc2vC7vKjqzrxBJucD8rpSaUjqiBnY/QuRJ0QeV717Uz5tg==}
engines: {node: '>=18'}
- '@ai-sdk/provider@0.0.26':
- resolution: {integrity: sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==}
+ '@ai-sdk/provider@1.1.3':
+ resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
engines: {node: '>=18'}
- '@ai-sdk/provider@2.0.0':
- resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==}
- engines: {node: '>=18'}
-
- '@ai-sdk/react@0.0.70':
- resolution: {integrity: sha512-GnwbtjW4/4z7MleLiW+TOZC2M29eCg1tOUpuEiYFMmFNZK8mkrqM0PFZMo6UsYeUYMWqEOOcPOU9OQVJMJh7IQ==}
+ '@ai-sdk/react@1.2.12':
+ resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
- zod: ^3.0.0
- peerDependenciesMeta:
- react:
- optional: true
- zod:
- optional: true
-
- '@ai-sdk/solid@0.0.54':
- resolution: {integrity: sha512-96KWTVK+opdFeRubqrgaJXoNiDP89gNxFRWUp0PJOotZW816AbhUf4EnDjBjXTLjXL1n0h8tGSE9sZsRkj9wQQ==}
- engines: {node: '>=18'}
- peerDependencies:
- solid-js: ^1.7.7
- peerDependenciesMeta:
- solid-js:
- optional: true
-
- '@ai-sdk/svelte@0.0.57':
- resolution: {integrity: sha512-SyF9ItIR9ALP9yDNAD+2/5Vl1IT6kchgyDH8xkmhysfJI6WrvJbtO1wdQ0nylvPLcsPoYu+cAlz1krU4lFHcYw==}
- engines: {node: '>=18'}
- peerDependencies:
- svelte: ^3.0.0 || ^4.0.0 || ^5.0.0
- peerDependenciesMeta:
- svelte:
- optional: true
-
- '@ai-sdk/ui-utils@0.0.50':
- resolution: {integrity: sha512-Z5QYJVW+5XpSaJ4jYCCAVG7zIAuKOOdikhgpksneNmKvx61ACFaf98pmOd+xnjahl0pIlc/QIe6O4yVaJ1sEaw==}
- engines: {node: '>=18'}
- peerDependencies:
- zod: ^3.0.0
+ zod: ^3.23.8
peerDependenciesMeta:
zod:
optional: true
- '@ai-sdk/vue@0.0.59':
- resolution: {integrity: sha512-+ofYlnqdc8c4F6tM0IKF0+7NagZRAiqBJpGDJ+6EYhDW8FHLUP/JFBgu32SjxSxC6IKFZxEnl68ZoP/Z38EMlw==}
+ '@ai-sdk/ui-utils@1.2.11':
+ resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==}
engines: {node: '>=18'}
peerDependencies:
- vue: ^3.3.4
- peerDependenciesMeta:
- vue:
- optional: true
+ zod: ^3.23.8
'@ampproject/remapping@2.3.0':
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
@@ -2122,9 +2077,6 @@ packages:
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
- '@standard-schema/spec@1.0.0':
- resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
-
'@stylistic/eslint-plugin-ts@2.13.0':
resolution: {integrity: sha512-nooe1oTwz60T4wQhZ+5u0/GAu3ygkKF9vPPZeRn/meG71ntQ0EZXVOKEonluAYl/+CV2T+nN0dknHa4evAW13Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2395,35 +2347,6 @@ packages:
'@vitest/utils@2.1.9':
resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
- '@vue/compiler-core@3.4.30':
- resolution: {integrity: sha512-ZL8y4Xxdh8O6PSwfdZ1IpQ24PjTAieOz3jXb/MDTfDtANcKBMxg1KLm6OX2jofsaQGYfIVzd3BAG22i56/cF1w==}
-
- '@vue/compiler-dom@3.4.30':
- resolution: {integrity: sha512-+16Sd8lYr5j/owCbr9dowcNfrHd+pz+w2/b5Lt26Oz/kB90C9yNbxQ3bYOvt7rI2bxk0nqda39hVcwDFw85c2Q==}
-
- '@vue/compiler-sfc@3.4.30':
- resolution: {integrity: sha512-8vElKklHn/UY8+FgUFlQrYAPbtiSB2zcgeRKW7HkpSRn/JjMRmZvuOtwDx036D1aqKNSTtXkWRfqx53Qb+HmMg==}
-
- '@vue/compiler-ssr@3.4.30':
- resolution: {integrity: sha512-ZJ56YZGXJDd6jky4mmM0rNaNP6kIbQu9LTKZDhcpddGe/3QIalB1WHHmZ6iZfFNyj5mSypTa4+qDJa5VIuxMSg==}
-
- '@vue/reactivity@3.4.30':
- resolution: {integrity: sha512-bVJurnCe3LS0JII8PPoAA63Zd2MBzcKrEzwdQl92eHCcxtIbxD2fhNwJpa+KkM3Y/A4T5FUnmdhgKwOf6BfbcA==}
-
- '@vue/runtime-core@3.4.30':
- resolution: {integrity: sha512-qaFEbnNpGz+tlnkaualomogzN8vBLkgzK55uuWjYXbYn039eOBZrWxyXWq/7qh9Bz2FPifZqGjVDl/FXiq9L2g==}
-
- '@vue/runtime-dom@3.4.30':
- resolution: {integrity: sha512-tV6B4YiZRj5QsaJgw2THCy5C1H+2UeywO9tqgWEc21tn85qHEERndHN/CxlyXvSBFrpmlexCIdnqPuR9RM9thw==}
-
- '@vue/server-renderer@3.4.30':
- resolution: {integrity: sha512-TBD3eqR1DeDc0cMrXS/vEs/PWzq1uXxnvjoqQuDGFIEHFIwuDTX/KWAQKIBjyMWLFHEeTDGYVsYci85z2UbTDg==}
- peerDependencies:
- vue: 3.4.30
-
- '@vue/shared@3.4.30':
- resolution: {integrity: sha512-CLg+f8RQCHQnKvuHY9adMsMaQOcqclh6Z5V9TaoMgy0ut0tz848joZ7/CYFFyF/yZ5i2yaw7Fn498C+CNZVHIg==}
-
'@web3-storage/multipart-parser@1.0.0':
resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==}
@@ -2477,26 +2400,15 @@ packages:
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
engines: {node: '>=8'}
- ai@3.4.33:
- resolution: {integrity: sha512-plBlrVZKwPoRTmM8+D1sJac9Bq8eaa2jiZlHLZIWekKWI1yMWYZvCCEezY9ASPwRhULYDJB2VhKOBUUeg3S5JQ==}
+ ai@4.3.19:
+ resolution: {integrity: sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==}
engines: {node: '>=18'}
peerDependencies:
- openai: ^4.42.0
react: ^18 || ^19 || ^19.0.0-rc
- sswr: ^2.1.0
- svelte: ^3.0.0 || ^4.0.0 || ^5.0.0
- zod: ^3.0.0
+ zod: ^3.23.8
peerDependenciesMeta:
- openai:
- optional: true
react:
optional: true
- sswr:
- optional: true
- svelte:
- optional: true
- zod:
- optional: true
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -2531,10 +2443,6 @@ packages:
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
engines: {node: '>=10'}
- aria-query@5.3.2:
- resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
- engines: {node: '>= 0.4'}
-
array-flatten@1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
@@ -2559,10 +2467,6 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
- axobject-query@4.1.0:
- resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
- engines: {node: '>= 0.4'}
-
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
@@ -2763,9 +2667,6 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
- code-red@1.0.4:
- resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
-
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -3042,10 +2943,6 @@ packages:
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
- entities@4.5.0:
- resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
- engines: {node: '>=0.12'}
-
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
@@ -3257,10 +3154,6 @@ packages:
resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==}
engines: {node: '>=14.18'}
- eventsource-parser@3.0.6:
- resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
- engines: {node: '>=18.0.0'}
-
evp_bytestokey@1.0.3:
resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==}
@@ -3825,9 +3718,6 @@ packages:
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
engines: {node: '>=14'}
- locate-character@3.0.0:
- resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
-
locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
@@ -5074,11 +4964,6 @@ packages:
resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
- sswr@2.2.0:
- resolution: {integrity: sha512-clTszLPZkmycALTHD1mXGU+mOtA/MIoLgS1KGTTzFNVm9rytQVykgRaP+z1zl572cz0bTqj4rFVoC2N+IGK4Sg==}
- peerDependencies:
- svelte: ^4.0.0 || ^5.0.0
-
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
@@ -5168,23 +5053,11 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
- svelte@4.2.18:
- resolution: {integrity: sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA==}
- engines: {node: '>=16'}
-
swr@2.3.6:
resolution: {integrity: sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
- swrev@4.0.0:
- resolution: {integrity: sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==}
-
- swrv@1.1.0:
- resolution: {integrity: sha512-pjllRDr2s0iTwiE5Isvip51dZGR7GjLH1gCSVyE8bQnbAx6xackXsFdojau+1O5u98yHF5V73HQGOFxKUXO9gQ==}
- peerDependencies:
- vue: '>=3.2.26 < 4'
-
synckit@0.11.11:
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -5599,14 +5472,6 @@ packages:
vm-browserify@1.1.2:
resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==}
- vue@3.4.30:
- resolution: {integrity: sha512-NcxtKCwkdf1zPsr7Y8+QlDBCGqxvjLXF2EX+yi76rV5rrz90Y6gK1cq0olIhdWGgrlhs9ElHuhi9t3+W5sG5Xw==}
- peerDependencies:
- typescript: '*'
- peerDependenciesMeta:
- typescript:
- optional: true
-
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
@@ -5740,19 +5605,10 @@ snapshots:
'@ai-sdk/provider-utils': 1.0.9(zod@3.25.76)
zod: 3.25.76
- '@ai-sdk/google@2.0.14(zod@3.25.76)':
+ '@ai-sdk/google@1.2.22(zod@3.25.76)':
dependencies:
- '@ai-sdk/provider': 2.0.0
- '@ai-sdk/provider-utils': 3.0.9(zod@3.25.76)
- zod: 3.25.76
-
- '@ai-sdk/provider-utils@1.0.22(zod@3.25.76)':
- dependencies:
- '@ai-sdk/provider': 0.0.26
- eventsource-parser: 1.1.2
- nanoid: 3.3.11
- secure-json-parse: 2.7.0
- optionalDependencies:
+ '@ai-sdk/provider': 1.1.3
+ '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76)
zod: 3.25.76
'@ai-sdk/provider-utils@1.0.9(zod@3.25.76)':
@@ -5764,71 +5620,37 @@ snapshots:
optionalDependencies:
zod: 3.25.76
- '@ai-sdk/provider-utils@3.0.9(zod@3.25.76)':
+ '@ai-sdk/provider-utils@2.2.8(zod@3.25.76)':
dependencies:
- '@ai-sdk/provider': 2.0.0
- '@standard-schema/spec': 1.0.0
- eventsource-parser: 3.0.6
+ '@ai-sdk/provider': 1.1.3
+ nanoid: 3.3.11
+ secure-json-parse: 2.7.0
zod: 3.25.76
'@ai-sdk/provider@0.0.17':
dependencies:
json-schema: 0.4.0
- '@ai-sdk/provider@0.0.26':
+ '@ai-sdk/provider@1.1.3':
dependencies:
json-schema: 0.4.0
- '@ai-sdk/provider@2.0.0':
+ '@ai-sdk/react@1.2.12(react@18.3.1)(zod@3.25.76)':
dependencies:
- json-schema: 0.4.0
-
- '@ai-sdk/react@0.0.70(react@18.3.1)(zod@3.25.76)':
- dependencies:
- '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76)
- '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76)
+ '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76)
+ '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76)
+ react: 18.3.1
swr: 2.3.6(react@18.3.1)
throttleit: 2.1.0
optionalDependencies:
- react: 18.3.1
zod: 3.25.76
- '@ai-sdk/solid@0.0.54(zod@3.25.76)':
+ '@ai-sdk/ui-utils@1.2.11(zod@3.25.76)':
dependencies:
- '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76)
- '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76)
- transitivePeerDependencies:
- - zod
-
- '@ai-sdk/svelte@0.0.57(svelte@4.2.18)(zod@3.25.76)':
- dependencies:
- '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76)
- '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76)
- sswr: 2.2.0(svelte@4.2.18)
- optionalDependencies:
- svelte: 4.2.18
- transitivePeerDependencies:
- - zod
-
- '@ai-sdk/ui-utils@0.0.50(zod@3.25.76)':
- dependencies:
- '@ai-sdk/provider': 0.0.26
- '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76)
- json-schema: 0.4.0
- secure-json-parse: 2.7.0
- zod-to-json-schema: 3.24.6(zod@3.25.76)
- optionalDependencies:
+ '@ai-sdk/provider': 1.1.3
+ '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76)
zod: 3.25.76
-
- '@ai-sdk/vue@0.0.59(vue@3.4.30(typescript@5.9.2))(zod@3.25.76)':
- dependencies:
- '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76)
- '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76)
- swrv: 1.1.0(vue@3.4.30(typescript@5.9.2))
- optionalDependencies:
- vue: 3.4.30(typescript@5.9.2)
- transitivePeerDependencies:
- - zod
+ zod-to-json-schema: 3.24.6(zod@3.25.76)
'@ampproject/remapping@2.3.0':
dependencies:
@@ -7463,8 +7285,6 @@ snapshots:
'@shikijs/vscode-textmate@10.0.2': {}
- '@standard-schema/spec@1.0.0': {}
-
'@stylistic/eslint-plugin-ts@2.13.0(eslint@9.35.0(jiti@1.21.7))(typescript@5.9.2)':
dependencies:
'@typescript-eslint/utils': 8.43.0(eslint@9.35.0(jiti@1.21.7))(typescript@5.9.2)
@@ -7903,60 +7723,6 @@ snapshots:
loupe: 3.2.1
tinyrainbow: 1.2.0
- '@vue/compiler-core@3.4.30':
- dependencies:
- '@babel/parser': 7.28.4
- '@vue/shared': 3.4.30
- entities: 4.5.0
- estree-walker: 2.0.2
- source-map-js: 1.2.1
-
- '@vue/compiler-dom@3.4.30':
- dependencies:
- '@vue/compiler-core': 3.4.30
- '@vue/shared': 3.4.30
-
- '@vue/compiler-sfc@3.4.30':
- dependencies:
- '@babel/parser': 7.28.4
- '@vue/compiler-core': 3.4.30
- '@vue/compiler-dom': 3.4.30
- '@vue/compiler-ssr': 3.4.30
- '@vue/shared': 3.4.30
- estree-walker: 2.0.2
- magic-string: 0.30.19
- postcss: 8.5.6
- source-map-js: 1.2.1
-
- '@vue/compiler-ssr@3.4.30':
- dependencies:
- '@vue/compiler-dom': 3.4.30
- '@vue/shared': 3.4.30
-
- '@vue/reactivity@3.4.30':
- dependencies:
- '@vue/shared': 3.4.30
-
- '@vue/runtime-core@3.4.30':
- dependencies:
- '@vue/reactivity': 3.4.30
- '@vue/shared': 3.4.30
-
- '@vue/runtime-dom@3.4.30':
- dependencies:
- '@vue/reactivity': 3.4.30
- '@vue/runtime-core': 3.4.30
- '@vue/shared': 3.4.30
- csstype: 3.1.3
-
- '@vue/server-renderer@3.4.30(vue@3.4.30(typescript@5.9.2))':
- dependencies:
- '@vue/compiler-ssr': 3.4.30
- '@vue/shared': 3.4.30
- vue: 3.4.30(typescript@5.9.2)
-
- '@vue/shared@3.4.30': {}
-
'@web3-storage/multipart-parser@1.0.0': {}
'@webcontainer/api@1.3.0-internal.10': {}
@@ -7998,29 +7764,17 @@ snapshots:
clean-stack: 2.2.0
indent-string: 4.0.0
- ai@3.4.33(react@18.3.1)(sswr@2.2.0(svelte@4.2.18))(svelte@4.2.18)(vue@3.4.30(typescript@5.9.2))(zod@3.25.76):
+ ai@4.3.19(react@18.3.1)(zod@3.25.76):
dependencies:
- '@ai-sdk/provider': 0.0.26
- '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76)
- '@ai-sdk/react': 0.0.70(react@18.3.1)(zod@3.25.76)
- '@ai-sdk/solid': 0.0.54(zod@3.25.76)
- '@ai-sdk/svelte': 0.0.57(svelte@4.2.18)(zod@3.25.76)
- '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76)
- '@ai-sdk/vue': 0.0.59(vue@3.4.30(typescript@5.9.2))(zod@3.25.76)
+ '@ai-sdk/provider': 1.1.3
+ '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76)
+ '@ai-sdk/react': 1.2.12(react@18.3.1)(zod@3.25.76)
+ '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76)
'@opentelemetry/api': 1.9.0
- eventsource-parser: 1.1.2
- json-schema: 0.4.0
jsondiffpatch: 0.6.0
- secure-json-parse: 2.7.0
- zod-to-json-schema: 3.24.6(zod@3.25.76)
+ zod: 3.25.76
optionalDependencies:
react: 18.3.1
- sswr: 2.2.0(svelte@4.2.18)
- svelte: 4.2.18
- zod: 3.25.76
- transitivePeerDependencies:
- - solid-js
- - vue
ajv@6.12.6:
dependencies:
@@ -8052,8 +7806,6 @@ snapshots:
dependencies:
tslib: 2.8.1
- aria-query@5.3.2: {}
-
array-flatten@1.1.1: {}
as-table@1.0.55:
@@ -8082,8 +7834,6 @@ snapshots:
dependencies:
possible-typed-array-names: 1.1.0
- axobject-query@4.1.0: {}
-
bail@2.0.2: {}
balanced-match@1.0.2: {}
@@ -8321,14 +8071,6 @@ snapshots:
clsx@2.1.1: {}
- code-red@1.0.4:
- dependencies:
- '@jridgewell/sourcemap-codec': 1.5.5
- '@types/estree': 1.0.8
- acorn: 8.15.0
- estree-walker: 3.0.3
- periscopic: 3.1.0
-
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -8581,8 +8323,6 @@ snapshots:
dependencies:
once: 1.4.0
- entities@4.5.0: {}
-
entities@6.0.1: {}
err-code@2.0.3: {}
@@ -8910,8 +8650,6 @@ snapshots:
eventsource-parser@1.1.2: {}
- eventsource-parser@3.0.6: {}
-
evp_bytestokey@1.0.3:
dependencies:
md5.js: 1.3.5
@@ -9563,8 +9301,6 @@ snapshots:
pkg-types: 2.3.0
quansync: 0.2.11
- locate-character@3.0.0: {}
-
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
@@ -11338,11 +11074,6 @@ snapshots:
dependencies:
minipass: 7.1.2
- sswr@2.2.0(svelte@4.2.18):
- dependencies:
- svelte: 4.2.18
- swrev: 4.0.0
-
stackback@0.0.2: {}
stacktracey@2.1.8:
@@ -11433,35 +11164,12 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
- svelte@4.2.18:
- dependencies:
- '@ampproject/remapping': 2.3.0
- '@jridgewell/sourcemap-codec': 1.5.5
- '@jridgewell/trace-mapping': 0.3.31
- '@types/estree': 1.0.8
- acorn: 8.15.0
- aria-query: 5.3.2
- axobject-query: 4.1.0
- code-red: 1.0.4
- css-tree: 2.3.1
- estree-walker: 3.0.3
- is-reference: 3.0.3
- locate-character: 3.0.0
- magic-string: 0.30.19
- periscopic: 3.1.0
-
swr@2.3.6(react@18.3.1):
dependencies:
dequal: 2.0.3
react: 18.3.1
use-sync-external-store: 1.5.0(react@18.3.1)
- swrev@4.0.0: {}
-
- swrv@1.1.0(vue@3.4.30(typescript@5.9.2)):
- dependencies:
- vue: 3.4.30(typescript@5.9.2)
-
synckit@0.11.11:
dependencies:
'@pkgr/core': 0.2.9
@@ -11957,16 +11665,6 @@ snapshots:
vm-browserify@1.1.2: {}
- vue@3.4.30(typescript@5.9.2):
- dependencies:
- '@vue/compiler-dom': 3.4.30
- '@vue/compiler-sfc': 3.4.30
- '@vue/runtime-dom': 3.4.30
- '@vue/server-renderer': 3.4.30(vue@3.4.30(typescript@5.9.2))
- '@vue/shared': 3.4.30
- optionalDependencies:
- typescript: 5.9.2
-
w3c-keyname@2.2.8: {}
wcwidth@1.0.1:
From 7fe440243fafff3da8eddbe31da03216378ee9b8 Mon Sep 17 00:00:00 2001
From: azamjb
Date: Sat, 13 Sep 2025 16:59:22 -0400
Subject: [PATCH 18/53] solve page
---
app/components/challenge/ChallengeCard.tsx | 49 ++++++
app/routes/_index.tsx | 167 ++++++++++++++++++++-
app/routes/profile.tsx | 35 +++--
app/styles/variables.scss | 144 +++++++++---------
public/Folders.png | Bin 0 -> 105693 bytes
public/login.png | Bin 0 -> 81177 bytes
public/profile.jpg | Bin 0 -> 64583 bytes
public/sales-dashboard.png | Bin 0 -> 65047 bytes
8 files changed, 304 insertions(+), 91 deletions(-)
create mode 100644 app/components/challenge/ChallengeCard.tsx
create mode 100644 public/Folders.png
create mode 100644 public/login.png
create mode 100644 public/profile.jpg
create mode 100644 public/sales-dashboard.png
diff --git a/app/components/challenge/ChallengeCard.tsx b/app/components/challenge/ChallengeCard.tsx
new file mode 100644
index 0000000000..36df83241b
--- /dev/null
+++ b/app/components/challenge/ChallengeCard.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+
+export type ChallengeCardProps = {
+ id: string;
+ title: string;
+ image: string;
+ difficulty: 'Easy' | 'Medium' | 'Hard';
+ averageAccuracy?: number; // percentage, optional for backward compatibility
+ onClick?: () => void;
+};
+
+export function ChallengeCard({ id, title, image, difficulty, averageAccuracy, onClick }: ChallengeCardProps) {
+ const difficultyColor =
+ difficulty === 'Easy' ? 'text-green-500' : difficulty === 'Medium' ? 'text-yellow-500' : 'text-red-500';
+ return (
+
+
+

+
+
+
+
+ {title}
+
+
+ {typeof averageAccuracy === 'number' && (
+
+ {averageAccuracy}%
+
+
+ )}
+ {difficulty}
+
+
+
+
+ );
+}
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
index 86d73409c9..9ab8bc5076 100644
--- a/app/routes/_index.tsx
+++ b/app/routes/_index.tsx
@@ -3,6 +3,72 @@ import { ClientOnly } from 'remix-utils/client-only';
import { BaseChat } from '~/components/chat/BaseChat';
import { Chat } from '~/components/chat/Chat.client';
import { Header } from '~/components/header/Header';
+import React, { useState } from 'react';
+import { ChallengeCard } from '~/components/challenge/ChallengeCard';
+import { useNavigate } from '@remix-run/react';
+
+type Challenge = {
+ id: string;
+ title: string;
+ image: string;
+ difficulty: 'Easy' | 'Medium' | 'Hard';
+ averageAccuracy: number;
+ description?: string;
+};
+
+const challenges: Challenge[] = [
+ {
+ id: '1',
+ title: 'Sales Dashboard',
+ image: '/sales-dashboard.png',
+ difficulty: 'Hard',
+ averageAccuracy: 62,
+ },
+ {
+ id: '2',
+ title: 'Login Box',
+ image: '/login.png',
+ difficulty: 'Easy',
+ averageAccuracy: 91,
+ },
+ {
+ id: '3',
+ title: 'Google Drive',
+ image: '/Folders.png',
+ difficulty: 'Easy',
+ averageAccuracy: 87,
+ },
+ {
+ id: '4',
+ title: 'Profile Page',
+ image: '/profile.jpg',
+ difficulty: 'Medium',
+ averageAccuracy: 74,
+ description: 'Determine whether an integer is a palindrome.',
+ },
+ {
+ id: '5',
+ title: 'Merge Intervals',
+ image: '/project-visibility.jpg',
+ difficulty: 'Medium',
+ averageAccuracy: 68,
+ description: 'Merge all overlapping intervals in a list of intervals.',
+ },
+ {
+ id: '6',
+ title: 'N-Queens',
+ image: '/social_preview_index.jpg',
+ difficulty: 'Hard',
+ averageAccuracy: 41,
+ description: 'Place N queens on an N×N chessboard so that no two queens threaten each other.',
+ },
+] as const;
+
+const difficultyOptions = ['All', 'Easy', 'Medium', 'Hard'] as const;
+const sortOptions = [
+ { value: 'title', label: 'Title' },
+ { value: 'difficulty', label: 'Difficulty' },
+];
export const meta: MetaFunction = () => {
return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
@@ -11,10 +77,109 @@ export const meta: MetaFunction = () => {
export const loader = () => json({});
export default function Index() {
+ const navigate = useNavigate();
+ const [difficulty, setDifficulty] = useState<'All' | 'Easy' | 'Medium' | 'Hard'>('All');
+ const [sort, setSort] = useState<'title' | 'difficulty'>('title');
+ const [search, setSearch] = useState('');
+
+ const filtered = challenges.filter(
+ (c) =>
+ (difficulty === 'All' || c.difficulty === difficulty) &&
+ (c.title.toLowerCase().includes(search.toLowerCase()) ||
+ (c.description && c.description.toLowerCase().includes(search.toLowerCase()))),
+ );
+ const sorted = [...filtered].sort((a, b) => {
+ if (sort === 'title') {
+ return a.title.localeCompare(b.title);
+ }
+
+ if (sort === 'difficulty') {
+ return a.difficulty.localeCompare(b.difficulty);
+ }
+
+ return 0;
+ });
+
return (
-
}>{() =>
}
+
+
+
+
+
+
+ Solve Challenges
+
+
+ Browse and solve interactive UI challenges to sharpen your frontend skills.
+
+
+
+
+
+
setSearch(e.target.value)}
+ className="flex-1 rounded-lg px-4 py-2 border-0 bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-accent/60 transition shadow-md text-base font-medium placeholder:text-bolt-elements-textSecondary"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {sorted.map((challenge) => (
+ navigate(`/challenge/${challenge.id}`)} />
+ ))}
+
+
+
);
}
diff --git a/app/routes/profile.tsx b/app/routes/profile.tsx
index 8a282776e7..b2a675f08d 100644
--- a/app/routes/profile.tsx
+++ b/app/routes/profile.tsx
@@ -48,7 +48,6 @@ export default function ProfilePage() {