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
4 changes: 4 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
# Get your API key at: https://openrouter.ai/
OPENROUTER_API_KEY=

# Web Search API Key (Optional - enables web search functionality)
# Get your API key at: https://exa.ai/
# EXA_API_KEY=

# =============================================================================
# TERMINAL EXECUTION SETTINGS
# =============================================================================
Expand Down
5 changes: 5 additions & 0 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import type { ChatMode, ExecutionMode } from "@/types";
import { checkRateLimit } from "@/lib/rate-limit";
import { ChatSDKError } from "@/lib/errors";
import PostHogClient from "@/app/posthog";
import { geolocation } from "@vercel/functions";
import { getAIHeaders } from "@/lib/actions";

export const maxDuration = 300;

Expand All @@ -29,6 +31,7 @@ export async function POST(req: NextRequest) {

// Get user ID from authenticated session or fallback to anonymous
const userID = await getUserID(req);
const userLocation = geolocation(req);

// Check rate limit for the user
await checkRateLimit(userID);
Expand Down Expand Up @@ -59,6 +62,7 @@ export async function POST(req: NextRequest) {
writer,
mode,
executionMode,
userLocation,
);

// Generate title in parallel if this is the start of a conversation
Expand Down Expand Up @@ -92,6 +96,7 @@ export async function POST(req: NextRequest) {
messages: convertToModelMessages(truncatedMessages),
tools,
abortSignal: req.signal,
headers: getAIHeaders(),
experimental_transform: smoothStream({ chunking: "word" }),
stopWhen: stepCountIs(25),
onChunk: async (chunk) => {
Expand Down
4 changes: 2 additions & 2 deletions app/components/ComputerCodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,9 @@ export const ComputerCodeBlock = ({
delay={150}
addDefaultStyles={false}
showLanguage={false}
className={`shiki not-prose relative bg-transparent text-sm font-[450] text-card-foreground h-full w-full [&_pre]:!bg-transparent [&_pre]:px-[0.5em] [&_pre]:py-[0.5em] [&_pre]:rounded-none [&_pre]:m-0 [&_pre]:h-full [&_pre]:w-full [&_pre]:min-h-full ${
className={`shiki not-prose relative bg-transparent text-sm font-[450] text-card-foreground h-full w-full [&_pre]:!bg-transparent [&_pre]:px-[0.5em] [&_pre]:py-[0.5em] [&_pre]:rounded-none [&_pre]:m-0 [&_pre]:h-full [&_pre]:w-full [&_pre]:min-h-full [&_pre]:min-w-0 ${
wrap
? "[&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_pre]:overflow-visible"
? "[&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_pre]:overflow-visible [&_pre]:word-break-break-word"
: "[&_pre]:overflow-x-auto [&_pre]:max-w-full"
}`}
>
Expand Down
152 changes: 112 additions & 40 deletions app/components/ComputerSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import React from "react";
import { Minimize2, Edit } from "lucide-react";
import { Minimize2, Edit, Terminal } from "lucide-react";
import { useState } from "react";
import { useGlobalState } from "../contexts/GlobalState";
import { ComputerCodeBlock } from "./ComputerCodeBlock";
import { TerminalCodeBlock } from "./TerminalCodeBlock";
import { CodeActionButtons } from "@/components/ui/code-action-buttons";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { isSidebarFile, isSidebarTerminal } from "@/types/chat";

export const ComputerSidebar: React.FC = () => {
const { sidebarOpen, sidebarFile, closeSidebar } = useGlobalState();
const { sidebarOpen, sidebarContent, closeSidebar } = useGlobalState();
const [isWrapped, setIsWrapped] = useState(true);

if (!sidebarOpen || !sidebarFile) {
if (!sidebarOpen || !sidebarContent) {
return null;
}

const isFile = isSidebarFile(sidebarContent);
const isTerminal = isSidebarTerminal(sidebarContent);

const getLanguageFromPath = (filePath: string): string => {
const extension = filePath.split(".").pop()?.toLowerCase() || "";
const languageMap: Record<string, string> = {
Expand Down Expand Up @@ -62,13 +67,47 @@ export const ComputerSidebar: React.FC = () => {
};

const getActionText = (): string => {
const actionMap = {
reading: "Reading file",
creating: "Creating file",
editing: "Editing file",
writing: "Writing file",
};
return actionMap[sidebarFile.action || "reading"];
if (isFile) {
const actionMap = {
reading: "Reading file",
creating: "Creating file",
editing: "Editing file",
writing: "Writing file",
};
return actionMap[sidebarContent.action || "reading"];
} else if (isTerminal) {
return sidebarContent.isExecuting
? "Executing command"
: "Command executed";
}
return "Unknown action";
};

const getIcon = () => {
if (isFile) {
return <Edit className="w-5 h-5 text-muted-foreground" />;
} else if (isTerminal) {
return <Terminal className="w-5 h-5 text-muted-foreground" />;
}
return <Edit className="w-5 h-5 text-muted-foreground" />;
};

const getToolName = (): string => {
if (isFile) {
return "Editor";
} else if (isTerminal) {
return "Terminal";
}
return "Tool";
};

const getDisplayTarget = (): string => {
if (isFile) {
return sidebarContent.path.split("/").pop() || sidebarContent.path;
} else if (isTerminal) {
return sidebarContent.command;
}
return "";
};

const handleClose = () => {
Expand Down Expand Up @@ -115,74 +154,107 @@ export const ComputerSidebar: React.FC = () => {
{/* Action Status */}
<div className="flex items-center gap-2 mt-2">
<div className="w-[40px] h-[40px] bg-muted/50 rounded-lg flex items-center justify-center flex-shrink-0">
<Edit className="w-5 h-5 text-muted-foreground" />
{getIcon()}
</div>
<div className="flex-1 flex flex-col gap-1 min-w-0">
<div className="text-[12px] text-muted-foreground">
HackerAI is using{" "}
<span className="text-foreground">Editor</span>
<span className="text-foreground">{getToolName()}</span>
</div>
<div
title={`${getActionText()} ${sidebarFile.path}`}
title={`${getActionText()} ${getDisplayTarget()}`}
className="max-w-[100%] w-[max-content] truncate text-[13px] rounded-full inline-flex items-center px-[10px] py-[3px] border border-border bg-muted/30 text-foreground"
>
{getActionText()}
<span className="flex-1 min-w-0 px-1 ml-1 text-[12px] font-mono max-w-full text-ellipsis overflow-hidden whitespace-nowrap text-muted-foreground">
<code>
{sidebarFile.path.split("/").pop() || sidebarFile.path}
</code>
<code>{getDisplayTarget()}</code>
</span>
</div>
</div>
</div>

{/* Code Container */}
{/* Content Container */}
<div className="flex flex-col rounded-lg overflow-hidden bg-muted/20 border border-border/30 dark:border-black/30 shadow-[0px_4px_32px_0px_rgba(0,0,0,0.04)] flex-1 min-h-0 mt-[16px]">
{/* File Header */}
{/* Unified Header */}
<div className="h-[36px] flex items-center justify-between px-3 w-full bg-muted/30 border-b border-border rounded-t-lg shadow-[inset_0px_1px_0px_0px_rgba(255,255,255,0.1)]">
{/* Filename - far left */}
<div className="flex items-center">
<div className="max-w-[250px] truncate text-muted-foreground text-sm font-medium">
{sidebarFile.path.split("/").pop() || sidebarFile.path}
</div>
{/* Title - far left */}
<div className="flex items-center gap-2">
{isTerminal ? (
<Terminal
size={14}
className="text-muted-foreground flex-shrink-0"
/>
) : (
<div className="max-w-[250px] truncate text-muted-foreground text-sm font-medium">
{sidebarContent.path.split("/").pop() ||
sidebarContent.path}
</div>
)}
</div>

{/* Action buttons - far right */}
<CodeActionButtons
content={sidebarFile.content}
filename={sidebarFile.path.split("/").pop() || "code.txt"}
content={
isFile
? sidebarContent.content
: sidebarContent.output
? `$ ${sidebarContent.command}\n${sidebarContent.output}`
: `$ ${sidebarContent.command}`
}
filename={
isFile
? sidebarContent.path.split("/").pop() || "code.txt"
: "terminal-output.txt"
}
language={
sidebarFile.language ||
getLanguageFromPath(sidebarFile.path)
isFile
? sidebarContent.language ||
getLanguageFromPath(sidebarContent.path)
: "ansi"
}
isWrapped={isWrapped}
onToggleWrap={handleToggleWrap}
variant="sidebar"
/>
</div>

{/* Code Content */}
{/* Content */}
<div className="flex-1 min-h-0 w-full overflow-hidden">
<div className="flex flex-col min-h-0 h-full relative">
<div className="focus-visible:outline-none flex-1 min-h-0 h-full text-sm flex flex-col py-0 outline-none">
<div
className="font-mono w-full text-xs leading-[18px] flex-1 min-h-0 h-full"
className="font-mono w-full text-xs leading-[18px] flex-1 min-h-0 h-full min-w-0"
style={{
overflowWrap: "break-word",
wordBreak: "normal",
wordBreak: "break-word",
whiteSpace: "pre-wrap",
}}
>
<ComputerCodeBlock
language={
sidebarFile.language ||
getLanguageFromPath(sidebarFile.path)
}
wrap={isWrapped}
showButtons={false}
>
{sidebarFile.content}
</ComputerCodeBlock>
{isFile && (
<ComputerCodeBlock
language={
sidebarContent.language ||
getLanguageFromPath(sidebarContent.path)
}
wrap={isWrapped}
showButtons={false}
>
{sidebarContent.content}
</ComputerCodeBlock>
)}
{isTerminal && (
<TerminalCodeBlock
command={sidebarContent.command}
output={sidebarContent.output}
isExecuting={sidebarContent.isExecuting}
isBackground={sidebarContent.isBackground}
status={
sidebarContent.isExecuting ? "streaming" : "ready"
}
variant="sidebar"
wrap={isWrapped}
/>
)}
</div>
</div>
</div>
Expand Down
40 changes: 40 additions & 0 deletions app/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use client";

import React from "react";
import { useAppAuth } from "../hooks/useAppAuth";

const Footer: React.FC = () => {
const { user, loading } = useAppAuth();

if (loading || user) {
return null;
}

return (
<div className="text-muted-foreground relative flex min-h-8 w-full items-center justify-center p-4 text-center text-xs md:px-[60px] flex-shrink-0">
<span className="text-sm leading-none">
By messaging HackerAI, you agree to our{" "}
<a
href="/terms-of-service"
target="_blank"
className="text-foreground underline decoration-foreground"
rel="noreferrer"
>
Terms
</a>{" "}
and have read our{" "}
<a
href="/privacy-policy"
target="_blank"
className="text-foreground underline decoration-foreground"
rel="noreferrer"
>
Privacy Policy
</a>
.
</span>
</div>
);
};

export default Footer;
Loading