Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ OPENROUTER_API_KEY=
# Sign up at https://e2b.dev/
E2B_API_KEY=

# Model configuration for AI providers
# Agent model used for main chat interactions (default: qwen/qwen3-coder)
# NEXT_PUBLIC_AGENT_MODEL=

# Title generation model - should be lightweight to reduce cost/latency (default: qwen/qwen3-30b-a3)
# NEXT_PUBLIC_TITLE_MODEL=

# WorkOS config for auth (Optional)
# WORKOS_API_KEY='sk_example_123456789'
# WORKOS_CLIENT_ID='client_123456789'
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ Before running the application, you need to obtain API keys for the following se
OPENROUTER_API_KEY=your_openrouter_api_key_here
E2B_API_KEY=your_e2b_api_key_here
```
- Optionally customize the AI models:
```
NEXT_PUBLIC_AGENT_MODEL=qwen/qwen3-coder
NEXT_PUBLIC_TITLE_MODEL=qwen/qwen-turbo
```
Comment on lines +34 to +38
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify README examples match provider defaults (potential mismatch for title model).

The README example uses NEXT_PUBLIC_TITLE_MODEL=qwen/qwen-turbo. Ensure this aligns with the actual default configured in lib/ai/providers.ts and the placeholder in .env.local.example (AI summary indicates a default of qwen/qwen3-30b-a3). Aligning these avoids confusion.

Run this script to confirm defaults and placeholders:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for provider defaults and env usage..."
rg -n -A 3 -B 3 'NEXT_PUBLIC_(AGENT|TITLE)_MODEL|agent-model|title-model' lib --hidden || true

echo
echo "Checking example env file for placeholders..."
fd -a '.env.local.example' | while read -r f; do
  echo "File: $f"
  rg -n 'NEXT_PUBLIC_(AGENT|TITLE)_MODEL' "$f" -A 1 -B 1 || true
done

Length of output: 1348


Align README title model example with configured defaults

The README currently suggests using NEXT_PUBLIC_TITLE_MODEL=qwen/qwen-turbo, but in lib/ai/providers.ts the fallback is qwen/qwen3-30b-a3b (and the .env.local.example comments reference qwen/qwen3-30b-a3). Update to keep these in sync and avoid confusion:

• In README.md (around lines 34–38), replace:

-     NEXT_PUBLIC_TITLE_MODEL=qwen/qwen-turbo
+     NEXT_PUBLIC_TITLE_MODEL=qwen/qwen3-30b-a3b

• Optionally, verify that .env.local.example’s default comment (default: qwen/qwen3-30b-a3) matches the actual code default or adjust it to qwen/qwen3-30b-a3b.

📝 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
- Optionally customize the AI models:
```
NEXT_PUBLIC_AGENT_MODEL=qwen/qwen3-coder
NEXT_PUBLIC_TITLE_MODEL=qwen/qwen-turbo
```
- Optionally customize the AI models:
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

35-35: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In README.md around lines 34 to 38, the example title model value conflicts with
the code default in lib/ai/providers.ts; update the README example to use
NEXT_PUBLIC_TITLE_MODEL=qwen/qwen3-30b-a3b so it matches the fallback used by
the code, and also verify the comment in .env.local.example to ensure its
default comment reads qwen/qwen3-30b-a3b (or change the code/defaults to
consistently use whichever variant you prefer) so all three places (README,
.env.local.example comment, and lib/ai/providers.ts) are identical.


3. **Run the development server:**

Expand Down
20 changes: 16 additions & 4 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
stepCountIs,
streamText,
UIMessage,
smoothStream,
} from "ai";
import { openrouter } from "@openrouter/ai-sdk-provider";
import { systemPrompt } from "@/lib/system-prompt";
import { truncateMessagesToTokenLimit } from "@/lib/token-utils";
import { createTools } from "@/lib/ai/tools";
Expand All @@ -16,6 +16,7 @@ import { authkit } from "@workos-inc/authkit-nextjs";
import { generateTitleFromUserMessage } from "@/lib/actions";
import { NextRequest } from "next/server";
import type { ChatMode } from "@/types/chat";
import { myProvider } from "@/lib/ai/providers";

// Allow streaming responses up to 300 seconds
export const maxDuration = 300;
Expand All @@ -24,7 +25,7 @@ export async function POST(req: NextRequest) {
const { messages, mode }: { messages: UIMessage[]; mode: ChatMode } =
await req.json();

const model = "anthropic/claude-sonnet-4";
const model = "agent-model";

// Get user ID from authenticated session or fallback to anonymous
const getUserID = async (): Promise<string> => {
Expand All @@ -41,7 +42,7 @@ export async function POST(req: NextRequest) {

const userID = await getUserID();

// Truncate messages to stay within token limit
// Truncate messages to stay within token limit (processing is now done on frontend)
const truncatedMessages = truncateMessagesToTokenLimit(messages);

const stream = createUIMessageStream({
Expand Down Expand Up @@ -72,12 +73,23 @@ export async function POST(req: NextRequest) {
: Promise.resolve();

const result = streamText({
model: openrouter(model),
model: myProvider.languageModel("agent-model"),
system: systemPrompt(model),
messages: convertToModelMessages(truncatedMessages),
tools,
abortSignal: req.signal,
experimental_transform: smoothStream({ chunking: "word" }),
stopWhen: stepCountIs(25),
onError: async (error) => {
console.error("Error:", error);

// Perform same cleanup as onFinish to prevent resource leaks
const sandbox = getSandbox();
if (sandbox) {
await pauseSandbox(sandbox);
}
await titlePromise;
},
onFinish: async () => {
const sandbox = getSandbox();
if (sandbox) {
Expand Down
2 changes: 1 addition & 1 deletion app/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const ChatInput = ({

// Handle keyboard shortcuts for stopping generation
useHotkeys(
"ctrl+c, meta+c",
"ctrl+c",
(e) => {
e.preventDefault();
onStop();
Expand Down
119 changes: 8 additions & 111 deletions app/components/CodeHighlight.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import type { ReactNode } from "react";
import { useState } from "react";
import { Download, Copy, Check, WrapText } from "lucide-react";
import ShikiHighlighter, { isInlineCode, type Element } from "react-shiki";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { toast } from "sonner";
import { CodeActionButtons } from "@/components/ui/code-action-buttons";

interface CodeHighlightProps {
className?: string | undefined;
Expand All @@ -25,68 +19,10 @@ export const CodeHighlight = ({
const language = match ? match[1] : undefined;
const codeContent = String(children);

const [copied, setCopied] = useState(false);
const [isWrapped, setIsWrapped] = useState(false);

const isInline: boolean | undefined = node ? isInlineCode(node) : undefined;

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(codeContent.trim());
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error("Failed to copy code:", error);
}
};

const handleDownload = async () => {
const defaultFilename = `code.${language || "txt"}`;

try {
// Try to use the File System Access API for native save dialog
if ("showSaveFilePicker" in window) {
const fileHandle = await (
window as Window & {
showSaveFilePicker: (options: {
suggestedName: string;
}) => Promise<FileSystemFileHandle>;
}
).showSaveFilePicker({
suggestedName: defaultFilename,
});

const writable = await fileHandle.createWritable();
await writable.write(codeContent);
await writable.close();
toast.success("File saved successfully");
return;
}
} catch {
toast.error("Failed to save file");
return;
}

// Fallback to traditional download (will still show native save dialog on macOS)
try {
const blob = new Blob([codeContent], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = defaultFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success("File downloaded successfully");
} catch (error) {
toast.error("Failed to download file", {
description:
error instanceof Error ? error.message : "Unknown error occurred",
});
}
};

const handleToggleWrap = () => {
setIsWrapped(!isWrapped);
};
Expand All @@ -105,52 +41,13 @@ export const CodeHighlight = ({
</div>

{/* Right side - Action buttons */}
<div className="flex items-center space-x-2">
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={handleDownload}
className="p-1.5 opacity-70 hover:opacity-100 transition-opacity rounded hover:bg-secondary text-muted-foreground"
aria-label="Download"
>
<Download size={14} />
</button>
</TooltipTrigger>
<TooltipContent>Download</TooltipContent>
</Tooltip>

<Tooltip>
<TooltipTrigger asChild>
<button
onClick={handleToggleWrap}
className={`p-1.5 transition-all rounded hover:bg-secondary text-muted-foreground ${
isWrapped ? "opacity-100 bg-secondary" : "opacity-70"
}`}
aria-label={
isWrapped ? "Disable text wrapping" : "Enable text wrapping"
}
>
<WrapText size={14} />
</button>
</TooltipTrigger>
<TooltipContent>
{isWrapped ? "Disable text wrapping" : "Enable text wrapping"}
</TooltipContent>
</Tooltip>

<Tooltip>
<TooltipTrigger asChild>
<button
onClick={handleCopy}
className="p-1.5 opacity-70 hover:opacity-100 transition-opacity rounded hover:bg-secondary text-muted-foreground"
aria-label={copied ? "Copied!" : "Copy"}
>
{copied ? <Check size={14} /> : <Copy size={14} />}
</button>
</TooltipTrigger>
<TooltipContent>{copied ? "Copied!" : "Copy"}</TooltipContent>
</Tooltip>
</div>
<CodeActionButtons
content={codeContent}
language={language}
isWrapped={isWrapped}
onToggleWrap={handleToggleWrap}
variant="codeblock"
/>
</div>

{/* Code content */}
Expand Down
Loading