Merge branch 'main' into feat/allow-user-change-base-branch

This commit is contained in:
HeshamHM28 2025-11-12 05:49:08 -08:00 committed by GitHub
commit 26b3c95c5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1763 additions and 782 deletions

View file

@ -66,5 +66,5 @@ def is_codeflash_employee(user_id: str) -> bool:
return user_id in CODEFLASH_EMPLOYEE_GITHUB_IDS
def should_hack_for_demo(user_id: str, source_code: str) -> bool:
return bool(is_codeflash_employee(user_id) and "def find_common_tags(articles" in source_code)
def should_hack_for_demo(source_code: str) -> bool:
return bool("def find_common_tags(articles" in source_code)

View file

@ -217,7 +217,7 @@ async def optimize(
sentry_sdk.capture_exception(e)
return e.status_code, OptimizeErrorResponseSchema(error=e.message)
if should_hack_for_demo(request.user, ctx.source_code):
if should_hack_for_demo(ctx.source_code):
return 200, await hack_for_demo(ctx)
try:

View file

@ -345,7 +345,7 @@ async def testgen(
sentry_sdk.capture_exception(e)
return e.status_code, TestGenErrorResponseSchema(error=e.message)
if should_hack_for_demo(request.user, data.source_code_being_tested):
if should_hack_for_demo(data.source_code_being_tested):
demo_hack_response = await hack_for_demo(data, python_version)
return 200, demo_hack_response

View file

@ -7,6 +7,7 @@ import { vscode } from "./utils/vscode";
import ChatView from "./components/chatView";
import Tabs from "./components/tabs";
import OptimizationQueue from "./components/optimizationQueue";
import DemoOptimization from "./components/demoOptimization";
import SignInForm from "./components/signInForm";
function App() {
@ -28,20 +29,32 @@ function App() {
return <SignInForm />;
}
// https://cdn.displate.com/artwork/270x380/2024-11-23/60ffe7e4-4185-4ac8-9081-5268ba425a85.jpg
const firstTime = !store.triedCodeflashDemo;
return (
<div className="app-container">
<Tabs />
<div className="content">
{store.activeTab === "optimization" && <ChatView />}
{store.activeTab === "tasks" && (
<OptimizationQueue queueTasks={store.queueTasks} />
)}
</div>
{/* <OptimizeCurrentDiff /> */}
{/* {store.functionsInCurrentFile.length > 0 && <CurrentFileFunctions />} */}
{firstTime ? (
<>
<div className="gridBackground" />
<DemoOptimization />
</>
) : (
<>
<Tabs />
<div className="content">
{store.activeTab === "optimization" && <ChatView />}
{store.activeTab === "tasks" && (
<OptimizationQueue queueTasks={store.queueTasks} />
)}
</div>
</>
)}
</div>
);
}
// <OptimizeCurrentDiff />
// {store.functionsInCurrentFile.length > 0 && <CurrentFileFunctions />}
export default App;

View file

@ -61,12 +61,14 @@ interface ChatInputProps {
fileSuggestions?: FileInWorkspace[];
functionSuggestions?: { file: string; functions: string[] };
currentOptimizationTask: QueueTaskItem | undefined;
forDemo?: boolean;
}
const ChatInput: React.FC<ChatInputProps> = ({
export const ChatInput: React.FC<ChatInputProps> = ({
fileSuggestions = [],
functionSuggestions = { file: "", functions: [] },
currentOptimizationTask,
forDemo = false,
}) => {
const [outsideModuleRoot, setOutsideModuleRoot] = useState<boolean>(false);
@ -88,8 +90,12 @@ const ChatInput: React.FC<ChatInputProps> = ({
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (forDemo) {
return;
}
if (selectedFile) {
checkOutsideModuleRoot(selectedFile);
requestFunctionsFromFile(selectedFile);
} else {
setOutsideModuleRoot(false);
}
@ -242,7 +248,6 @@ const ChatInput: React.FC<ChatInputProps> = ({
const handleSelect = (choice: string | FileInWorkspace) => {
if (activeDropdown === "file") {
const file = choice as FileInWorkspace;
requestFunctionsFromFile(file.abs);
setSelectedFile(file.abs);
setSelectedFunction(undefined);
inputRef.current?.focus(); // focus the input to select the function
@ -379,7 +384,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
onKeyDown={handleKeyDown}
readOnly={isRunning}
/>
{selectedFile && outsideModuleRoot && (
{!forDemo && selectedFile && outsideModuleRoot && (
<div className={styles.outsideModuleWarning}>
<span className="codicon codicon-warning"></span>
<span>
@ -390,23 +395,29 @@ const ChatInput: React.FC<ChatInputProps> = ({
)}
</div>
)}
<Tooltip
position="left"
content={isRunning ? "Stop" : "Optimize"}
className={styles.tooltip}
>
<button
type="submit"
className={styles.button}
disabled={!selectedFile || !selectedFunction}
{forDemo ? (
currentOptimizationTask?.status == "optimizing" && (
<span className="codicon codicon-loading spin"></span>
)
) : (
<Tooltip
position="left"
content={isRunning ? "Stop" : "Optimize"}
className={styles.tooltip}
>
{isRunning ? (
<span className="codicon codicon-debug-stop"></span>
) : (
<span className="codicon codicon-arrow-up"></span>
)}
</button>
</Tooltip>
<button
type="submit"
className={styles.button}
disabled={!selectedFile || !selectedFunction}
>
{isRunning ? (
<span className="codicon codicon-debug-stop"></span>
) : (
<span className="codicon codicon-arrow-up"></span>
)}
</button>
</Tooltip>
)}
</div>
</div>

View file

@ -4,15 +4,15 @@
color: var(--vscode-editorWidget-foreground);
border: 1px solid var(--vscode-editorWidget-border, transparent);
border-radius: 7px;
padding: 0.15rem 0.4rem;
font-size: 0.75rem;
padding: 0.15rem 0.2rem;
font-size: 0.85rem;
display: inline-flex;
align-items: center;
gap: 0.2rem;
margin-right: 2px;
}
.chip :global(.codicon) {
.chip :global(.codicon).close {
padding: 0.1rem;
font-size: 0.8rem;
cursor: pointer;

View file

@ -12,7 +12,10 @@ const Chip = ({ children, onCloseClick, ...props }: Props) => {
>
{children}
{onCloseClick && (
<span className="codicon codicon-close" onClick={onCloseClick}></span>
<span
className={"codicon codicon-close " + styles.close}
onClick={onCloseClick}
></span>
)}
</span>
);

View file

@ -0,0 +1,114 @@
import { useStore } from "../store/root";
import { ChatInput } from "./chatView";
import TaskLogging from "./tasksLogging";
import styles from "../styles/demoWelcome.module.css";
import type {
MessageType,
RequestStartDemoOptimizationMessage,
} from "@codeflash/types";
declare const vscode: {
postMessage: (message: any) => void;
};
const DemoOptimization = () => {
const demoTask = useStore((state) => state.demoOptTask);
const handleStartDemo = () => {
const msg = {
type: "requestStartDemoOptimization",
} as RequestStartDemoOptimizationMessage;
vscode.postMessage(msg);
};
if (demoTask) {
return (
<>
<ChatInput
fileSuggestions={[]}
functionSuggestions={{ file: "", functions: [] }}
currentOptimizationTask={demoTask}
forDemo
/>
<TaskLogging
task={demoTask}
onDemoSuccess={() => {
vscode.postMessage({ type: "requestDemoOptimizationSuccess" });
}}
/>
</>
);
}
// Welcome screen for first-time users
return (
<div className={styles.welcomeContainer + " " + "gradiantBg"}>
<div className={styles.logoContainer}>
<div className="cf-logo">
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
viewBox="0 0 48.1 32"
>
<path
className="st0"
d="M31.8.3h-9.4L5.6,16.9h9.4L0,31.7h10.5L31.3,10.3h-9.6L31.8.3Z"
fill="black"
fill-rule="evenodd"
/>
<path className="st0" d="M34.6.3l-5.9,6.1h13.5L48,.3h-13.4Z" />
<path className="st0" d="M34.3,10.3l-5.9,6h13.5l5.8-6.1h-13.4Z" />
<path className="st0" d="M26.9,18.6l-5.9,6.1h13.5l5.8-6.1h-13.4Z" />
</svg>
</div>
<h1 className={styles.title}>Welcome to Codeflash</h1>
<p className={styles.subtitle}>
AI-powered Python optimization faster code, verified results.
</p>
</div>
<div className={styles.featuresGrid}>
<div className={styles.featureItem}>
<i className={`codicon codicon-zap ${styles.featureIcon}`}></i>
<span className={styles.featureText}>Optimize Python functions</span>
</div>
<div className={styles.featureItem}>
<i className={`codicon codicon-graph ${styles.featureIcon}`}></i>
<span className={styles.featureText}>Performance evaluation</span>
</div>
<div className={styles.featureItem}>
<i className={`codicon codicon-lightbulb ${styles.featureIcon}`}></i>
<span className={styles.featureText}>AI-powered suggestions</span>
</div>
</div>
<div className={styles.actionContainer}>
<p className={styles.helpText}>
Try Codeflash with a sample Python function to see how it works
</p>
<div className={styles.actionButtons}>
<button
className={styles.demoButton}
onClick={handleStartDemo}
title="Start a demo optimization to see Codeflash in action"
>
<i className={`codicon codicon-play ${styles.buttonIcon}`}></i>
Try it out
</button>
<button
className={styles.demoButton + " " + styles.skipButton}
onClick={() =>
vscode.postMessage({
type: "requestSkipDemoOptimization" as MessageType,
})
}
>
Skip Demo
</button>
</div>
</div>
</div>
);
};
export default DemoOptimization;

View file

@ -0,0 +1,39 @@
import React from "react";
import styles from "../styles/demoSuccess.module.css";
interface DemoSuccessProps {
onGetStarted?: () => void;
}
const DemoSuccess: React.FC<DemoSuccessProps> = ({ onGetStarted }) => {
const handleGetStarted = () => {
if (onGetStarted) {
onGetStarted();
}
};
return (
<div className={styles.demoSuccessContainer}>
<div className={styles.successIcon}>
<i className={`codicon codicon-check ${styles.successIconSymbol}`}></i>
</div>
<h2 className={styles.title}>Demo Completed Successfully!</h2>
<p className={styles.subtitle}>
You've seen how Codeflash can optimize your Python code with AI. Ready
to boost your own functions?
</p>
<button
className={styles.actionButton}
onClick={handleGetStarted}
title="Start optimizing your own Python code"
>
<i className={`codicon codicon-rocket ${styles.buttonIcon}`}></i>
Get Started
</button>
</div>
);
};
export default DemoSuccess;

View file

@ -3,132 +3,170 @@ import ReactMarkdown from "react-markdown";
import styles from "./markdown.module.css";
import CodeBlock from "../codeBlock/codeBlock";
import remarkGfm from "remark-gfm";
import Chip from "../chip/chip";
interface MarkdownBlockProps {
markdown?: string;
currentOptimizedFunction?: string;
}
const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
const components = useMemo(
() => ({
table: ({ children, ...props }: any) => {
return (
<div className="table-wrapper">
<table {...props}>{children}</table>
</div>
);
},
a: ({ href, children, ...props }: any) => {
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
// Only process file:// protocol or local file paths
const isLocalPath =
href?.startsWith("file://") ||
href?.startsWith("/") ||
!href?.includes("://");
const MarkdownBlock = memo(
({ markdown, currentOptimizedFunction }: MarkdownBlockProps) => {
const components = useMemo(
() => ({
table: ({ children, ...props }: any) => {
return (
<div className="table-wrapper">
<table {...props}>{children}</table>
</div>
);
},
a: ({ href, children, ...props }: any) => {
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
// Only process file:// protocol or local file paths
const isLocalPath =
href?.startsWith("file://") ||
href?.startsWith("/") ||
!href?.includes("://");
if (!isLocalPath) {
return;
if (!isLocalPath) {
return;
}
e.preventDefault();
// Handle absolute vs project-relative paths
let filePath = href.replace("file://", "");
// Extract line number if present
// const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
// let values = undefined
// if (match) {
// filePath = match[1]
// values = { line: parseInt(match[2]) }
// }
// Add ./ prefix if needed
if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
filePath = "./" + filePath;
}
// vscode.postMessage({
// type: "openFile",
// text: filePath,
// values,
// })
};
return (
<a {...props} href={href} onClick={handleClick}>
{children}
</a>
);
},
pre: ({ children }: any) => {
// The structure from react-markdown v9 is: pre > code > text
const codeEl = children as React.ReactElement;
if (!codeEl || !codeEl.props) {
return <pre>{children}</pre>;
}
e.preventDefault();
const { className = "", children: codeChildren } = codeEl.props as {
className?: string;
children?: React.ReactNode;
};
// Handle absolute vs project-relative paths
let filePath = href.replace("file://", "");
// Extract line number if present
// const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
// let values = undefined
// if (match) {
// filePath = match[1]
// values = { line: parseInt(match[2]) }
// }
// Add ./ prefix if needed
if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
filePath = "./" + filePath;
// Get the actual code text
let codeString = "";
if (typeof codeChildren === "string") {
codeString = codeChildren;
} else if (Array.isArray(codeChildren)) {
codeString = codeChildren
.filter((child) => typeof child === "string")
.join("");
}
// vscode.postMessage({
// type: "openFile",
// text: filePath,
// values,
// })
};
// Extract language from className
const match = /language-(\w+)/.exec(className);
const language = match ? match[1] : "text";
// Wrap CodeBlock in a div to ensure proper separation
return (
<div style={{ margin: "1em 0" }}>
<CodeBlock source={codeString} language={language!} />
</div>
);
},
code: ({ children, className, ...props }: any) => {
const isSingleLineString =
typeof children === "string" && children.split("\n").length === 1;
return (
<a {...props} href={href} onClick={handleClick}>
{children}
</a>
);
},
pre: ({ children }: any) => {
// The structure from react-markdown v9 is: pre > code > text
const codeEl = children as React.ReactElement;
const isFilePath = isSingleLineString && children.endsWith(".py");
const isTargetFunction =
isSingleLineString &&
currentOptimizedFunction &&
children === currentOptimizedFunction;
if (!codeEl || !codeEl.props) {
return <pre>{children}</pre>;
}
if (isFilePath) {
return (
<Chip
onClick={(e) => e.stopPropagation()}
style={{ background: "var(--code-block-bg" }}
>
<span
className="codicon codicon-python"
style={{ color: "var(--cyan)" }}
></span>
const { className = "", children: codeChildren } = codeEl.props as {
className?: string;
children?: React.ReactNode;
};
{children}
</Chip>
);
}
if (isTargetFunction) {
return (
<Chip
onClick={(e) => e.stopPropagation()}
style={{ background: "var(--code-block-bg" }}
>
<span className="codicon codicon-symbol-method"></span>
{children}
</Chip>
);
}
return (
<code className={className} {...props}>
{children}
</code>
);
},
}),
[],
);
// Get the actual code text
let codeString = "";
if (typeof codeChildren === "string") {
codeString = codeChildren;
} else if (Array.isArray(codeChildren)) {
codeString = codeChildren
.filter((child) => typeof child === "string")
.join("");
}
// Extract language from className
const match = /language-(\w+)/.exec(className);
const language = match ? match[1] : "text";
// Wrap CodeBlock in a div to ensure proper separation
return (
<div style={{ margin: "1em 0" }}>
<CodeBlock source={codeString} language={language!} />
</div>
);
},
code: ({ children, className, ...props }: any) => {
// This handles inline code
return (
<code className={className} {...props}>
{children}
</code>
);
},
}),
[],
);
return (
<div className={styles.markdown}>
<ReactMarkdown
remarkPlugins={[
remarkGfm,
// () => {
// return (tree: any) => {
// visit(tree, "code", (node: any) => {
// if (!node.lang) {
// node.lang = "text"
// } else if (node.lang.includes(".")) {
// node.lang = node.lang.split(".").slice(-1)[0]
// }
// })
// }
// },
]}
components={components}
>
{markdown || ""}
</ReactMarkdown>
</div>
);
});
return (
<div className={styles.markdown}>
<ReactMarkdown
remarkPlugins={[
remarkGfm,
// () => {
// return (tree: any) => {
// visit(tree, "code", (node: any) => {
// if (!node.lang) {
// node.lang = "text"
// } else if (node.lang.includes(".")) {
// node.lang = node.lang.split(".").slice(-1)[0]
// }
// })
// }
// },
]}
components={components}
>
{markdown || ""}
</ReactMarkdown>
</div>
);
},
);
export default MarkdownBlock;

View file

@ -186,24 +186,15 @@ const SignInForm = () => {
left: 0,
right: 0,
height: "100%",
background: "linear-gradient(to bottom, #FFC043, transparent 60%)",
opacity: 0.2,
background: "var(--codeflash-transparent-gradient-bg)",
opacity: 0.7,
pointerEvents: "none",
filter: "blur(60px)",
}}
/>
{/* Grid Background */}
<div
style={{
position: "absolute",
inset: 0,
backgroundImage:
"linear-gradient(to right, var(--vscode-editorLineNumber-foreground) 1px, transparent 1px), linear-gradient(to bottom, var(--vscode-editorLineNumber-foreground) 1px, transparent 1px)",
backgroundSize: "24px 24px",
opacity: 0.03,
}}
/>
<div className="gridBackground" />
{/* Main Container */}
<div

View file

@ -10,6 +10,7 @@ import styles from "./taskLogging.module.css";
import MarkdownBlock from "./markdown/markdown";
import CollapsibleFile from "./collapsible/collapsibleFile";
import CodeBlock from "./codeBlock/codeBlock";
import DemoSuccess from "./demoSuccess";
import { memo, useEffect, useRef } from "react";
import { vscode } from "../utils/vscode";
import {
@ -38,6 +39,7 @@ const LogItem = memo(
log,
isLast,
isStillRunning,
functionName,
}: {
log: LogEntry;
isLast: boolean;
@ -60,7 +62,10 @@ const LogItem = memo(
<span className="codicon codicon-pass-filled" />
)}
<div className={contentClassName}>
<MemoMarkdownBlock markdown={textLog.text} />
<MemoMarkdownBlock
markdown={textLog.text}
currentOptimizedFunction={functionName}
/>
</div>
</div>
);
@ -87,7 +92,10 @@ const LogItem = memo(
<span className="codicon codicon-pass-filled" />
)}
<div className={contentClassName}>
<MemoMarkdownBlock markdown={markdownLog.markdown} />
<MemoMarkdownBlock
markdown={markdownLog.markdown}
currentOptimizedFunction={functionName}
/>
</div>
</div>
);
@ -97,6 +105,7 @@ const LogItem = memo(
<div className={contentClassName}>
<MemoMarkdownBlock
markdown={`| Not supported log type: ${log.type}`}
currentOptimizedFunction={functionName}
/>
</div>
</div>
@ -105,7 +114,13 @@ const LogItem = memo(
},
);
const TaskLogging = ({ task }: { task: QueueTaskItem }) => {
const TaskLogging = ({
task,
onDemoSuccess,
}: {
task: QueueTaskItem;
onDemoSuccess?: () => void;
}) => {
const listRef = useRef<VirtuosoHandle>(null);
const scrollerRef = useRef<HTMLElement>(null);
@ -125,6 +140,49 @@ const TaskLogging = ({ task }: { task: QueueTaskItem }) => {
listRef.current?.scrollToIndex({ index: task.logs.length - 1 });
}, []);
const getFooterOnSuccess = () => {
return onDemoSuccess ? (
// completed the demo optimization
<div className={styles.logEntry}>
<DemoSuccess onGetStarted={onDemoSuccess} />
</div>
) : (
<div className={styles.logEntry}>
<div className={styles.logContent}>
<div
style={{
...footerContainerStyles,
color: "var(--vscode-foreground)",
background: "rgba(34, 134, 58, 0.15)",
}}
>
<MemoMarkdownBlock
markdown={`✅ Optimization completed successfully.`}
/>
<button
className={styles.queueTaskPatch}
onClick={() => {
const msg: ViewPatchMessage = {
type: "viewPatch",
payload: {
id: task.id,
functionName: task.functionName,
patchFile: task.patchFile!,
explanation: task.explanation || "",
speedupStr: task.speedupStr || "",
},
};
vscode.postMessage(msg);
}}
>
<span className="codicon codicon-eye"></span>
<span>View Optimization</span>
</button>
</div>
</div>
</div>
);
};
return (
<div className={styles.messagesContainer}>
<Virtuoso
@ -181,42 +239,7 @@ const TaskLogging = ({ task }: { task: QueueTaskItem }) => {
</div>
</div>
)}
{task.status === "completed" && (
<div className={styles.logEntry}>
<div className={styles.logContent}>
<div
style={{
...footerContainerStyles,
color: "var(--vscode-foreground)",
background: "rgba(34, 134, 58, 0.15)",
}}
>
<MemoMarkdownBlock
markdown={`✅ Optimization completed successfully.`}
/>
<button
className={styles.queueTaskPatch}
onClick={() => {
const msg: ViewPatchMessage = {
type: "viewPatch",
payload: {
id: task.id,
functionName: task.functionName,
patchFile: task.patchFile!,
explanation: task.explanation || "",
speedupStr: task.speedupStr || "",
},
};
vscode.postMessage(msg);
}}
>
<span className="codicon codicon-eye"></span>
<span>View Optimization</span>
</button>
</div>
</div>
</div>
)}
{task.status === "completed" && getFooterOnSuccess()}
</div>
);
};

View file

@ -2,7 +2,6 @@
box-sizing: border-box;
}
:root {
--codeflash-gradient-bg: linear-gradient(135deg, #ffc043, #685123);
--step-gap: 4rem;
}
@ -22,6 +21,8 @@ body {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* Header */
@ -31,37 +32,6 @@ body {
animation: fadeIn 0.6s ease;
}
.logo {
width: 48px;
height: 48px;
margin: 0 auto 16px;
padding: 0.6rem;
background-image: var(--codeflash-gradient-bg);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.logo::after {
content: "";
position: absolute;
display: block;
inset: -1px;
background-image: var(--codeflash-gradient-bg);
border-radius: 12px;
opacity: 0.2;
filter: blur(12px);
z-index: -1;
animation: pulse 3s ease-in-out infinite;
}
.logo .codicon {
font-size: 24px;
color: white;
}
h1 {
font-size: 24px;
font-weight: 600;

View file

@ -0,0 +1,17 @@
import type { LogEntry } from "@codeflash/types";
import type { State } from "../root";
export const handleAddLogForDemo = (state: State, log: LogEntry): State => {
const demoTask = state.demoOptTask;
if (!demoTask) {
return state;
}
return {
...state,
demoOptTask: {
...demoTask,
logs: [...demoTask.logs, log],
},
};
};

View file

@ -15,11 +15,13 @@ export const handleRestoreStateFromCache = (
running,
focusedTaskId,
queueTasks,
triedCodeflashDemo,
} = payload as {
running?: boolean;
queueTasks?: QueueTaskItem[];
focusedTaskId: string;
moduleRoot?: string;
triedCodeflashDemo?: boolean;
};
// console.log(
// `received restore state from cache message: ${message.type}, ${JSON.stringify(payload)}`,
@ -27,6 +29,7 @@ export const handleRestoreStateFromCache = (
return {
...oldState,
optimizationRunning: running ?? false,
triedCodeflashDemo: triedCodeflashDemo ?? false,
queueTasks: queueTasks ?? [],
focusedTaskId,
moduleRoot: moduleRoot ?? "",

View file

@ -0,0 +1,14 @@
import type { UpdateDemoOptimizationTaskMessage } from "@codeflash/types";
import type { State } from "../root";
export const updateDemoTask = (
oldState: State,
payload: UpdateDemoOptimizationTaskMessage["payload"],
): State => {
return {
...oldState,
demoOptTask: oldState.demoOptTask
? { ...oldState.demoOptTask, ...payload }
: null,
};
};

View file

@ -13,12 +13,15 @@ import { handleUpdateState } from "./actions/updateSidebarState";
import { handleUpdateQueueTasks } from "./actions/updateQueueTasks";
import { handleRestoreStateFromCache } from "./actions/restoreStateFromMemory";
import { handleAddLog } from "./actions/addLog";
import { handleAddLogForDemo } from "./actions/addLogForDemo";
import { updateDemoTask } from "./actions/updateDemoTask";
export type State = {
moduleRoot: string;
status: SidebarStatus;
activeTab: SidebarTab;
optimizationRunning: boolean;
triedCodeflashDemo: boolean;
queueTasks: QueueTaskItem[];
functionsInCurrentFile: string[];
focusedTaskId: string;
@ -26,6 +29,7 @@ export type State = {
filesInWorkspace: FileInWorkspace[];
functionsInCurrentContextFile: { file: string; functions: string[] };
isValidatingApiKey: boolean;
demoOptTask: QueueTaskItem | null;
};
export type Action = {
@ -42,7 +46,11 @@ export type Action = {
changeTaskFocus: (id: string) => void;
setActiveTab: (tab: SidebarTab) => void;
setApiKeyLoadingState: (loading: boolean) => void;
initDemoOptTask: (task: QueueTaskItem) => void;
addLog: (log: LogEntry) => void;
addLogForDemo: (log: LogEntry) => void;
clearDemoOptTask: () => void;
updateDemoOptTask: (taskUpdate: Partial<QueueTaskItem>) => void;
};
const initialState: State = {
@ -57,6 +65,8 @@ const initialState: State = {
filesInWorkspace: [],
functionsInCurrentContextFile: { file: "", functions: [] },
isValidatingApiKey: false,
triedCodeflashDemo: false,
demoOptTask: null,
};
export const useStore = create<State & Action>((_set) => ({
@ -85,4 +95,12 @@ export const useStore = create<State & Action>((_set) => ({
addLog: (log: LogEntry) => _set((oldState) => handleAddLog(oldState, log)),
setApiKeyLoadingState: (loading: boolean) =>
_set((oldState) => ({ ...oldState, isValidatingApiKey: loading })),
initDemoOptTask: (task: QueueTaskItem) =>
_set((oldState) => ({ ...oldState, demoOptTask: task })),
addLogForDemo: (log: LogEntry) =>
_set((oldState) => handleAddLogForDemo(oldState, log)),
updateDemoOptTask: (payload: Partial<QueueTaskItem>) =>
_set((oldState) => updateDemoTask(oldState, payload)),
clearDemoOptTask: () =>
_set((oldState) => ({ ...oldState, demoOptTask: null })),
}));

View file

@ -0,0 +1,235 @@
.demoSuccessContainer {
padding: 1.3rem;
background: linear-gradient(
135deg,
var(--vscode-editor-background) 0%,
var(--vscode-sideBar-background) 100%
);
border: 1px solid var(--vscode-panel-border);
border-radius: 12px;
margin: 16px 0;
text-align: center;
position: relative;
overflow: hidden;
animation: slideInSuccess 0.6s ease-out;
}
.successIcon {
width: 48px;
height: 48px;
background: var(--vscode-button-background);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px;
animation: bounceIn 0.8s ease-out 0.2s both;
}
.successIconSymbol {
font-size: 24px;
color: var(--vscode-button-foreground);
}
.title {
font-size: 18px;
font-weight: 600;
color: var(--vscode-foreground);
margin: 0 0 8px 0;
animation: fadeInUp 0.6s ease-out 0.4s both;
}
.subtitle {
font-size: 14px;
color: var(--vscode-descriptionForeground);
margin: 0 0 24px 0;
line-height: 1.4;
animation: fadeInUp 0.6s ease-out 0.6s both;
}
.actionButton {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
font-family: var(--vscode-font-family);
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
animation: fadeInUp 0.6s ease-out 0.8s both;
min-width: 140px;
}
.actionButton::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.2),
transparent
);
transition: left 0.5s ease;
}
.actionButton:hover {
background: var(--vscode-button-hoverBackground);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
.actionButton:hover::before {
left: 100%;
}
.actionButton:active {
transform: translateY(-1px);
}
.actionButton:focus {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
.buttonIcon {
font-size: 16px;
transition: transform 0.3s ease;
}
.actionButton:hover .buttonIcon {
transform: translateX(2px);
}
.features {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 20px;
flex-wrap: wrap;
animation: fadeInUp 0.6s ease-out 1s both;
}
.featureBadge {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
border-radius: 16px;
font-size: 12px;
font-weight: 500;
}
.featureBadgeIcon {
font-size: 12px;
}
/* Animations */
@keyframes slideInSuccess {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
transform: scale(1.1);
}
70% {
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes shimmer {
0%,
100% {
transform: translateX(-100%);
}
50% {
transform: translateX(100%);
}
}
/* Responsive design */
@media (max-width: 320px) {
.demoSuccessContainer {
padding: 20px 16px;
}
.features {
flex-direction: column;
align-items: center;
}
.actionButton {
width: 100%;
justify-content: center;
}
}
/* High contrast support */
@media (prefers-contrast: high) {
.demoSuccessContainer {
border-width: 2px;
}
.actionButton {
border: 2px solid var(--vscode-button-foreground);
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
.demoSuccessContainer,
.successIcon,
.title,
.subtitle,
.actionButton,
.features {
animation: none;
}
.actionButton:hover {
transform: none;
}
.actionButton::before {
display: none;
}
}

View file

@ -0,0 +1,239 @@
.welcomeContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 24px;
height: 100%;
text-align: center;
color: var(--vscode-foreground);
font-family: var(--vscode-font-family);
animation: fadeIn 0.5s ease-in-out;
}
.logoContainer {
margin-bottom: 24px;
opacity: 0;
animation: slideUp 0.6s ease-out 0.2s forwards;
}
.logo {
width: 64px;
height: 64px;
border-radius: 12px;
background: linear-gradient(
135deg,
var(--vscode-button-background) 0%,
var(--vscode-button-hoverBackground) 100%
);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.logoIcon {
font-size: 28px;
color: var(--vscode-button-foreground);
}
.title {
font-size: 24px;
font-weight: 600;
margin: 0 0 8px 0;
color: var(--vscode-foreground);
opacity: 0;
animation: slideUp 0.6s ease-out 0.4s forwards;
}
.subtitle {
font-size: 14px;
color: var(--vscode-descriptionForeground);
margin: 0 0 32px 0;
line-height: 1.5;
opacity: 0;
animation: slideUp 0.6s ease-out 0.6s forwards;
}
.featuresGrid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
width: 100%;
max-width: 300px;
margin-bottom: 32px;
opacity: 0;
animation: slideUp 0.6s ease-out 0.8s forwards;
}
.featureItem {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--vscode-input-background);
border: 1px solid var(--vscode-input-border);
border-radius: 8px;
transition: all 0.2s ease;
}
.featureItem:hover {
background: var(--vscode-list-hoverBackground);
border-color: var(--vscode-focusBorder);
}
.featureIcon {
font-size: 16px;
color: #ffc043;
min-width: 16px;
}
.featureText {
font-size: 13px;
color: var(--vscode-foreground);
text-align: left;
}
.actionContainer {
opacity: 0;
animation: slideUp 0.6s ease-out 1s forwards;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.actionButtons {
display: flex;
gap: 1rem;
align-items: center;
justify-content: center;
}
.demoButton {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
font-family: var(--vscode-font-family);
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.skipButton {
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
}
.demoButton:hover {
background: var(--vscode-button-hoverBackground);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.skipButton:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
.demoButton:active {
transform: translateY(0);
}
.demoButton:focus {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
.skipButton:focus {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
.buttonIcon {
font-size: 16px;
}
.helpText {
font-size: 12px;
color: var(--vscode-descriptionForeground);
margin-top: 16px;
opacity: 0;
animation: fadeIn 0.4s ease-out 1.2s forwards;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive adjustments for smaller panels */
@media (max-width: 320px) {
.welcomeContainer {
padding: 24px 16px;
}
.title {
font-size: 20px;
}
.logo {
width: 56px;
height: 56px;
}
.logoIcon {
font-size: 24px;
}
.featuresGrid {
max-width: 100%;
}
}
/* High contrast theme support */
@media (prefers-contrast: high) {
.featureItem {
border-width: 2px;
}
.demoButton {
border: 2px solid var(--vscode-button-foreground);
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
.welcomeContainer,
.logoContainer,
.title,
.subtitle,
.featuresGrid,
.actionContainer,
.helpText {
animation: none;
opacity: 1;
}
.demoButton:hover {
transform: none;
}
}

View file

@ -8,6 +8,17 @@
black 5%
);
/* --code-block-bg: var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))cc; */
--brand: #ffc043;
--cyan: #306998;
--codeflash-gradient-bg: linear-gradient(135deg, #ffc043, #685123);
--codeflash-transparent-gradient-bg: linear-gradient(
to bottom,
#ffc0434f,
transparent 60%
);
--duration: 5s;
--scale: 0.6;
--glow-size: 100%;
}
body {
@ -62,6 +73,34 @@ body {
flex-direction: column;
height: 100vh;
position: relative;
padding: 1rem 0;
z-index: 2;
}
.gradiantBg::before {
content: "";
position: absolute;
inset: 0;
background-image: var(--codeflash-transparent-gradient-bg);
opacity: 0.5;
pointer-events: none;
z-index: -1;
}
.gridBackground {
position: absolute;
inset: 0;
background-image:
linear-gradient(
to right,
var(--vscode-editorLineNumber-foreground) 1px,
transparent 1px
),
linear-gradient(
to bottom,
var(--vscode-editorLineNumber-foreground) 1px,
transparent 1px
);
background-size: 24px 24px;
opacity: 0.03;
}
.content {
@ -119,13 +158,6 @@ body {
}
/* end of api key container */
/* glowing animation */
:root {
--duration: 5s;
--scale: 0.6;
--glow-size: 100%;
}
/* core glow element (empty element positioned over the container) */
.glowEffect {
pointer-events: none;
@ -239,3 +271,34 @@ body {
.table-wrapper tr:hover td {
background-color: var(--vscode-list-hoverBackground);
}
.cf-logo {
width: 48px;
height: 48px;
margin: 0 auto 16px;
padding: 0.6rem;
background-image: var(--codeflash-gradient-bg);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.cf-logo::after {
content: "";
position: absolute;
display: block;
inset: -1px;
background-image: var(--codeflash-gradient-bg);
border-radius: 12px;
opacity: 0.2;
filter: blur(12px);
z-index: -1;
animation: pulse 3s ease-in-out infinite;
}
.cf-logo .codicon {
font-size: 24px;
color: white;
}

View file

@ -1,14 +1,23 @@
.container {
animation: fadeIn 0.3s ease-in-out;
padding: 16px;
background-color: var(--vscode-sideBar-background);
border: 1px solid var(--vscode-input-border);
position: relative;
border-radius: 8px;
font-family: var(--vscode-font-family);
color: var(--vscode-foreground);
overflow: hidden;
width: 100%;
height: 100%;
}
.container::after {
content: "";
display: block;
position: absolute;
inset: 0;
z-index: -1;
background-color: var(--vscode-sideBar-background);
opacity: 0.6;
}
.form {
display: flex;

View file

@ -1,4 +1,5 @@
import type {
AppendLogToDemoOptimizationTaskMessage,
ChangeTaskFocusFromBackendMessage,
NewLogEntryMessage,
RecievedFilesInWorkspace,
@ -6,6 +7,8 @@ import type {
RestoreStateMessage,
SetActiveSidebarTabMessage,
SetApiKeyLoadingStateMessage,
StartDemoOptimizationMessage,
UpdateDemoOptimizationTaskMessage,
UpdateQueueTasksMessage,
UpdateStateMessage,
WebviewMessage,
@ -60,5 +63,25 @@ export const messageHandler = (event: MessageEvent, store: State & Action) => {
.payload;
store.setApiKeyLoadingState(loading);
break;
case "startDemoOptimization":
const { task } = (webviewMessage as StartDemoOptimizationMessage).payload;
store.initDemoOptTask(task);
break;
case "updateDemoOptimizationTask":
const payload = (webviewMessage as UpdateDemoOptimizationTaskMessage)
.payload;
store.updateDemoOptTask(payload);
break;
case "appendLogToDemoOptimizationTask":
const { log: demoLog } = (
webviewMessage as AppendLogToDemoOptimizationTaskMessage
).payload;
store.addLogForDemo(demoLog);
break;
case "clearDemoOptimizationTask":
store.clearDemoOptTask();
break;
default:
console.warn("Unhandled message type:", webviewMessage.type);
}
};

View file

@ -26,6 +26,14 @@ export type MessageType =
| "setActiveSidebarTab"
| "setApiKeyLoadingState"
| "cancelTask"
| "submitInitForm"
| "appendLogToDemoOptimizationTask"
| "startDemoOptimization"
| "clearDemoOptimizationTask"
| "requestStartDemoOptimization"
| "updateDemoOptimizationTask"
| "requestSkipDemoOptimization"
| "requestDemoOptimizationSuccess"
| "signIn"
| "authStarted"
| "authCompleted"
@ -130,6 +138,27 @@ export interface UpdateQueueTasksMessage extends WebviewMessage {
};
}
export interface AppendLogToDemoOptimizationTaskMessage extends WebviewMessage {
type: "appendLogToDemoOptimizationTask";
payload: {
log: LogEntry;
};
}
export interface UpdateDemoOptimizationTaskMessage extends WebviewMessage {
type: "updateDemoOptimizationTask";
payload: Partial<QueueTaskItem>;
}
export interface ClearDemoOptimizationTaskMessage extends WebviewMessage {
type: "clearDemoOptimizationTask";
}
export interface StartDemoOptimizationMessage extends WebviewMessage {
type: "startDemoOptimization";
payload: {
task: QueueTaskItem;
};
}
export interface SetApiKeyLoadingStateMessage extends WebviewMessage {
type: "setApiKeyLoadingState";
payload: {
@ -180,6 +209,18 @@ export interface CreateSampleFileMessage extends WebviewMessage {
type: "createSampleFile";
}
export interface RequestStartDemoOptimizationMessage extends WebviewMessage {
type: "requestStartDemoOptimization";
}
export interface RequestSkipDemoOptimizationMessage extends WebviewMessage {
type: "requestSkipDemoOptimization";
}
export interface RequestDemoOptimizationSuccessMessage extends WebviewMessage {
type: "requestDemoOptimizationSuccess";
}
export interface ChangeTaskFocusMessage extends WebviewMessage {
type: "changeTaskFocus";
payload: {
@ -352,6 +393,9 @@ export type IncomingWebviewMessage =
| SignInMessage
| ChangeTaskFocusMessage
| SubmitInitFormMessage
| RequestStartDemoOptimizationMessage
| RequestSkipDemoOptimizationMessage
| RequestDemoOptimizationSuccessMessage
| CancelAuthMessage;
export type OutgoingWebviewMessage =
@ -365,6 +409,11 @@ export type OutgoingWebviewMessage =
| RecievedFunctionsMessage
| ChangeTaskFocusFromBackendMessage
| SetActiveSidebarTabMessage
| SetApiKeyLoadingStateMessage
| AppendLogToDemoOptimizationTaskMessage
| StartDemoOptimizationMessage
| UpdateDemoOptimizationTaskMessage
| ClearDemoOptimizationTaskMessage
| AuthStartedMessage
| AuthCompletedMessage
| AuthCancelledMessage

View file

@ -24,6 +24,7 @@ export const LSP_COMMANDS = {
PERFORM_FUNCTION_OPTIMIZATION: "performFunctionOptimization",
CLEANUP_CURRENT_OPTIMIZER_SESSION: "cleanupCurrentOptimizerSession",
INIT_PROJECT: "initProject",
START_DEMO_OPTIMIZATION: "startDemoOptimization",
} as const;
export const PYTHON_EXTENSION_ID = "ms-python.python";

View file

@ -4,6 +4,7 @@ import { window, workspace } from "vscode";
export const enum GlobalStateKey {
IsExtensionInstalled = "isExtensionInstalled",
TriedCodeflashDemo = "triedCodeflashDemo",
Running = "running",
UserID = "user_id",
QueueTasks = "queueTasks",
@ -18,6 +19,7 @@ export type ContextFiles = Record<string, string>;
interface GlobalStateKeyMapping {
[GlobalStateKey.Running]: boolean;
[GlobalStateKey.IsExtensionInstalled]: boolean;
[GlobalStateKey.TriedCodeflashDemo]: boolean;
[GlobalStateKey.UserID]: string | undefined;
[GlobalStateKey.QueueTasks]: QueueTaskItem[];
[GlobalStateKey.FocusedTaskId]: string | undefined;
@ -89,6 +91,7 @@ export class GlobalState {
focusedTaskId: this.get(GlobalStateKey.FocusedTaskId)!,
queueTasks: this.get(GlobalStateKey.QueueTasks, [])!,
moduleRoot: this.get(GlobalStateKey.ModuleRoot)!,
triedCodeflashDemo: this.get(GlobalStateKey.TriedCodeflashDemo, false)!,
};
}

View file

@ -79,6 +79,11 @@ export class CodeflashCodeLensProvider
return [];
}
// don't provide codelens until user has tried the demo optimization once
if (!this.globalState.get(GlobalStateKey.TriedCodeflashDemo, false)) {
return [];
}
if (this.lspClient.state !== LanguageClientState.Running) {
this.logger.warn(
`CodeLens requested but LSP client not running. State: ${this.lspClient.state}`,

View file

@ -206,7 +206,12 @@ export class GitPatchProvider {
}
private removeFilesPrefixes = (path: string): string => {
return path.replace("a/", "").replace("b/", "");
if (path.startsWith("a/")) {
return path.replace("a/", "");
} else if (path.startsWith("b/")) {
return path.replace("b/", "");
}
return path;
};
async applyPatch() {
@ -215,10 +220,10 @@ export class GitPatchProvider {
const patchContent = await fs.readFile(patchPath, { encoding: "utf-8" });
applyPatches(patchContent, {
loadFile: (index, callback) => {
const filePath = this.removeFilesPrefixes(
path.join(this.workspaceRoot, index.newFileName),
const filePath = path.join(
this.workspaceRoot,
this.removeFilesPrefixes(index.newFileName),
);
fs.readFile(filePath, { encoding: "utf-8" })
.then((content) => callback(null, content))
.catch((err) => callback(err, ""));

View file

@ -248,55 +248,61 @@ export class InitWebviewProvider
private getHtmlContent(metaTags: string, scriptTag: string): string {
return `<!DOCTYPE html>
<html lang="en">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Codeflash</title>
${metaTags}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Codeflash</title>
${metaTags}
</head>
<body>
<div id="initview-container">
<div class="header">
<div class="logo">
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
viewBox="0 0 48.1 32"
>
<defs>
<style>
.st0 {
fill: black;
fill-rule: evenodd;
}
</style>
</defs>
<path
class="st0"
d="M31.8.3h-9.4L5.6,16.9h9.4L0,31.7h10.5L31.3,10.3h-9.6L31.8.3Z"
/>
<path class="st0" d="M34.6.3l-5.9,6.1h13.5L48,.3h-13.4Z" />
<path class="st0" d="M34.3,10.3l-5.9,6h13.5l5.8-6.1h-13.4Z" />
<path class="st0" d="M26.9,18.6l-5.9,6.1h13.5l5.8-6.1h-13.4Z" />
</svg>
</div>
<h1>Codeflash</h1>
<div id="initview-container" class="gradiantBg">
<div class="header">
<div class="cf-logo">
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
viewBox="0 0 48.1 32"
>
<defs>
<style>
.st0 {
fill: black;
fill-rule: evenodd;
}
</style>
</defs>
<path
class="st0"
d="M31.8.3h-9.4L5.6,16.9h9.4L0,31.7h10.5L31.3,10.3h-9.6L31.8.3Z"
/>
<path class="st0" d="M34.6.3l-5.9,6.1h13.5L48,.3h-13.4Z" />
<path class="st0" d="M34.3,10.3l-5.9,6h13.5l5.8-6.1h-13.4Z" />
<path class="st0" d="M26.9,18.6l-5.9,6.1h13.5l5.8-6.1h-13.4Z" />
</svg>
</div>
<h1>Codeflash</h1>
</div>
<div class="loading-wrapper" id="loading">
<div class="spinner"></div>
<p id="init-message">${this.currentInitMessage}</p>
<div class="wave-wrapper">
${[...Array(5)].map((_, i) => `<div class="wave-bar" style="animation-delay:${i * 100}ms" ></div>`).join("")}
</div>
<div class="spinner"></div>
<p id="init-message">${this.currentInitMessage}</p>
<div class="wave-wrapper">
${[...Array(5)]
.map(
(_, i) =>
`<div class="wave-bar" style="animation-delay:${i * 100}ms" ></div>`,
)
.join("")}
</div>
<div class="timeline" id="steps-container"></div>
</div>
${scriptTag}
<div class="timeline" id="steps-container"></div>
</div>
${scriptTag}
</body>
</html>`;
</html>
`;
}
override dispose() {

View file

@ -52,6 +52,7 @@ export class SidebarProvider
private _currentFocusUri: vscode.Uri | null = null;
private _currentFunctionCount = 0;
private initializedOnce = false;
private runningDemo = false;
private openedPathches = new Map<string, GitPatchProvider>();
constructor(
@ -115,7 +116,9 @@ export class SidebarProvider
private addLogEntryToUI(log: LogEntry) {
this.sendMessage({
type: "newLogEntry",
type: this.runningDemo
? "appendLogToDemoOptimizationTask"
: "newLogEntry",
payload: {
log,
},
@ -211,7 +214,7 @@ export class SidebarProvider
case "webviewReady":
this._logger.debug("Webview reported ready. restoring previous state");
this.retryUntilLspRunning(this.handleInitialState.bind(this));
this.restorePrevStateRequest();
this.syncUIState();
break;
case "navigateToFunction":
await this.handleNavigateToFunction(
@ -266,6 +269,15 @@ export class SidebarProvider
vscode.window.showWarningMessage(message);
}
break;
case "requestStartDemoOptimization":
await this.handleStartDemoOptimization();
break;
case "requestSkipDemoOptimization":
this.handleSkipDemoOptimization();
break;
case "requestDemoOptimizationSuccess":
this.handleDemoOptimizationSuccess();
break;
case "signIn":
await this.handleSignIn();
break;
@ -540,7 +552,67 @@ export class SidebarProvider
}
}
private restorePrevStateRequest(): void {
private async handleStartDemoOptimization(): Promise<void> {
try {
this.runningDemo = true;
const taskId = randomUUID().toString();
const functionName = "find_common_tags";
const demoTask: QueueTaskItem = {
id: taskId,
status: "optimizing",
functionName,
filepath: `${functionName}.py`,
logs: [],
};
this.sendMessage({
type: "startDemoOptimization",
payload: {
task: demoTask,
},
});
const initLog = this.getInitLog(taskId);
this.sendMessage({
type: "appendLogToDemoOptimizationTask",
payload: {
log: initLog,
},
});
const result =
await this._optimizationService.runDemoOptimizationTask(functionName);
if (result.status === "success") {
this.sendMessage({
type: "updateDemoOptimizationTask",
payload: {
status: "completed",
},
});
this.globalState.set(GlobalStateKey.TriedCodeflashDemo, true);
}
} finally {
this.runningDemo = false;
}
}
private handleDemoOptimizationSuccess(): void {
this.globalState.set(GlobalStateKey.TriedCodeflashDemo, true);
this.syncUIState();
this.sendMessage({
type: "clearDemoOptimizationTask",
});
this.refreshCodelens();
}
private handleSkipDemoOptimization(): void {
this.globalState.set(GlobalStateKey.TriedCodeflashDemo, true);
this.syncUIState();
this.refreshCodelens();
}
private syncUIState(): void {
const message: RestoreStateMessage = {
type: "restoreStateFromCache",
state: this.globalState.getStateForWebview(),
@ -564,6 +636,15 @@ export class SidebarProvider
await this.requestAnalysisFromServer(this._currentFocusUri);
}
private getInitLog(taskId: string): LogEntry {
return {
id: randomUUID(),
type: "text",
text: "initializing codeflash optimization process",
takes_time: true,
task_id: taskId,
};
}
private async handleNavigateToFunction(
functionName: string,
fileUri?: string,

View file

@ -60,14 +60,22 @@ export class LspService {
args,
transport: TransportKind.stdio,
options: {
env: { ...process.env, CODEFLASH_LSP: "true" },
env: {
...process.env,
CODEFLASH_LSP: "true",
},
},
},
debug: {
transport: TransportKind.stdio,
command,
args: args,
options: { env: { ...process.env, CODEFLASH_LSP: "true" } },
options: {
env: {
...process.env,
CODEFLASH_LSP: "true",
},
},
},
};

View file

@ -248,6 +248,20 @@ export class OptimizationService {
return result;
}
async runDemoOptimizationTask(
functionName: string,
): Promise<OptimizationResponse> {
const result =
await this.optimizationClient.sendRequest<OptimizationResponse>(
LSP_COMMANDS.START_DEMO_OPTIMIZATION,
{
functionName: functionName,
},
);
return result;
}
dispose(): void {
this.logger.dispose();
}

View file

@ -27,6 +27,7 @@ import LogoBox from "@/components/dashboard/logo-box"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { generateToken } from "@/app/(dashboard)/apikeys/tokenfuncs"
import { ApiKeyCard, StepInstruction } from "@/components/onboarding/step-content-renderer"
import { ConfettiEffect } from "@/components/onboarding/confetti-effect"
// Type definitions to improve type safety
interface ReferralOption {
@ -87,8 +88,7 @@ const INSTALLATION_COMMANDS = {
const ONBOARDING_STEPS = [
{ id: 0, title: "How did you hear about us?" },
{ id: 1, title: "Install & Initialize Codeflash" },
{ id: 2, title: "Install GitHub app" },
{ id: 3, title: "Setup Complete" },
{ id: 2, title: "Setup Complete" },
]
// Reusable animations
@ -105,6 +105,7 @@ export default function OnboardingPage() {
const [userId, setUserId] = useState<string>("")
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const [, setGithubAppInstalled] = useState(false)
const [showConfetti, setShowConfetti] = useState(false)
// Onboarding step management
const [stepsState, setStepsState] = useState<StepsState>({
@ -147,6 +148,22 @@ export default function OnboardingPage() {
}
}, [showIntro])
// Show confetti when Setup Complete step is shown
useEffect(() => {
const currentStepIndex = getCurrentStep()
if (currentStepIndex === 2) {
// Show confetti when Setup Complete step is displayed
setShowConfetti(true)
// Hide confetti after 5 seconds
const timer = setTimeout(() => {
setShowConfetti(false)
}, 5000)
return () => clearTimeout(timer)
} else {
setShowConfetti(false)
}
}, [stepsState.current.cli])
// Check if user has completed onboarding and load referral source if available
useEffect(() => {
const checkOnboardingStatus = async () => {
@ -256,7 +273,6 @@ export default function OnboardingPage() {
if (userId) {
await completeUserOnboarding(userId)
}
sessionStorage.setItem("showOnboardingConfetti", "true")
router.push("/app/apikeys")
} catch (error) {
console.error("Failed to mark onboarding as completed:", error)
@ -267,6 +283,9 @@ export default function OnboardingPage() {
const handleGitHubAppInstall = () => {
window.open("https://github.com/apps/codeflash-ai/installations/select_target", "_blank")
setGithubAppInstalled(true)
if (activeInitStep === 3) {
setActiveInitStep(4)
}
}
// Handle selecting a referral source and automatically submitting for non-other options
@ -355,7 +374,7 @@ export default function OnboardingPage() {
// Reusable Loading component
const LoadingSpinner = () => (
<div className="flex justify-center items-center">
<div className="flex justify-center items-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
</div>
)
@ -642,7 +661,7 @@ export default function OnboardingPage() {
<main className="flex-grow flex flex-col items-center justify-center max-w-3xl mx-auto -mt-12">
<motion.div {...fadeInAnimation} className="w-full">
<h1 className="text-2xl font-bold text-foreground mb-2">
<h1 className="text-2xl font-bold text-foreground mb-2 mt-12">
Install & Initialize Codeflash
</h1>
<p className="text-muted-foreground mb-8 font-medium">
@ -660,7 +679,7 @@ export default function OnboardingPage() {
{/* Installation Section */}
<div>
<h2 className="text-lg font-semibold text-foreground mb-4">
1. Install Codeflash
Install Codeflash
</h2>
<Tabs defaultValue="pip" className="w-full">
<TabsList className="w-full justify-start mb-4">
@ -686,7 +705,7 @@ export default function OnboardingPage() {
{/* Initialization Section */}
<div className="space-y-4">
<h2 className="text-lg font-semibold text-foreground mb-4">
2. Initialize Codeflash
Initialize Codeflash
</h2>
{/* Step 1: Run Initialization */}
@ -810,6 +829,47 @@ export default function OnboardingPage() {
</div>
)}
</StepInstruction>
{/* Step 3: Install GitHub App */}
<StepInstruction
number={3}
title="Install GitHub app"
status={
activeInitStep === 3
? "active"
: activeInitStep > 3
? "completed"
: "default"
}
>
<div className="space-y-4">
<p className="text-sm text-muted-foreground font-medium">
Codeflash requires this integration to open pull requests with the
optimizations for your review.
</p>
<div className="flex justify-center">
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.98 }}
onClick={handleGitHubAppInstall}
className="bg-[#24292f] hover:bg-[#0d1117] text-white transition-all duration-200
rounded-lg flex items-center justify-center gap-2 px-6 py-3 shadow-lg font-medium"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="white"
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
<span className="font-medium">Install GitHub App</span>
<ExternalLink className="w-4 h-4 ml-1" />
</motion.button>
</div>
</div>
</StepInstruction>
</div>
</div>
</motion.div>
@ -820,57 +880,10 @@ export default function OnboardingPage() {
</div>
)}
{/* Connect with GitHub Step */}
{/* Setup Complete Step */}
{currentStepIndex === 2 && (
<div className="min-h-screen flex flex-col p-6">
<Header />
<main className="flex-grow flex flex-col items-center justify-center max-w-3xl mx-auto -mt-12">
<motion.div {...fadeInAnimation} className="w-full">
<h1 className="text-2xl font-bold text-foreground mb-2">Install GitHub app</h1>
<p className="text-muted-foreground mb-8 font-medium">
Codeflash requires this integration to open pull requests with the optimizations
for your review.
</p>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="w-full p-6"
>
<div className="flex justify-center">
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.98 }}
onClick={handleGitHubAppInstall}
className="bg-[#24292f] hover:bg-[#0d1117] text-white transition-all duration-200
rounded-lg flex items-center justify-center gap-2 px-6 py-3 shadow-lg font-medium"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="white"
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
<span className="font-medium">Install GitHub App</span>
<ExternalLink className="w-4 h-4 ml-1" />
</motion.button>
</div>
</motion.div>
<NavigationButtons />
</motion.div>
</main>
</div>
)}
{/* Setup Complete Step */}
{currentStepIndex === 3 && (
<div className="min-h-screen flex flex-col p-6">
{showConfetti && <ConfettiEffect />}
<Header />
<main className="flex-grow flex flex-col items-center justify-center max-w-3xl mx-auto -mt-4">

View file

@ -1,39 +0,0 @@
"use client"
import { useState, useEffect, ReactNode } from "react"
import { ConfettiEffect } from "@/components/onboarding/confetti-effect"
interface ApiKeysClientProps {
children: ReactNode
}
export function ApiKeysClient({ children }: ApiKeysClientProps) {
const [showConfetti, setShowConfetti] = useState(false)
useEffect(() => {
// Check for the session storage flag
const shouldShowConfetti = sessionStorage.getItem("showOnboardingConfetti") === "true"
if (shouldShowConfetti) {
// Remove the flag so it only happens once
sessionStorage.removeItem("showOnboardingConfetti")
// Show the confetti
setShowConfetti(true)
// Optionally hide confetti after a few seconds
const timer = setTimeout(() => {
setShowConfetti(false)
}, 5000)
return () => clearTimeout(timer)
}
}, [])
return (
<>
{children}
{showConfetti && <ConfettiEffect />}
</>
)
}

View file

@ -5,7 +5,6 @@ import { Separator } from "@/components/ui/separator"
import { ApiKeyTable } from "./api-key-table"
import { type cf_api_keys, PrismaClient } from "@prisma/client"
import PostHogClient from "@/lib/posthog"
import { ApiKeysClient } from "./api-keys-client"
import { VS_CODE_KEY_NAME } from "@codeflash-ai/common"
const prisma = new PrismaClient()
@ -32,49 +31,47 @@ export default async function APIKeyGenerator(): Promise<JSX.Element> {
await posthog.shutdown()
return (
<ApiKeysClient>
<div>
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight max-w-full pb-1">
API Keys
</h3>
<Separator />
{apiKeys.length === 0 ? (
<>
<p className="leading-7 mt-6">
Welcome! Check out the{" "}
<a
href="https://docs.codeflash.ai/getting-started/local-installation"
target="_blank"
className="underline"
>
Getting Started
</a>{" "}
docs, or create your first API key below to start using Codeflash.
</p>
<p>
For help with setting up Codeflash on your codebase, please check out the Docs or{" "}
<a
href="https://calendly.com/codeflash-saurabh/codeflash-setup"
target="_blank"
className="underline"
>
book a call
</a>{" "}
with the founder.
</p>
</>
) : (
<>
{" "}
<p className="leading-7 mt-6">
These API keys are used to authenticate your requests to Codeflash&apos;s AI services.
</p>
<ApiKeyTable apiKeys={apiKeys} vscodeKeyName={VS_CODE_KEY_NAME} />{" "}
</>
)}
<div>
<h3 className="scroll-m-20 text-2xl font-semibold tracking-tight max-w-full pb-1">
API Keys
</h3>
<Separator />
{apiKeys.length === 0 ? (
<>
<p className="leading-7 mt-6">
Welcome! Check out the{" "}
<a
href="https://docs.codeflash.ai/getting-started/local-installation"
target="_blank"
className="underline"
>
Getting Started
</a>{" "}
docs, or create your first API key below to start using Codeflash.
</p>
<p>
For help with setting up Codeflash on your codebase, please check out the Docs or{" "}
<a
href="https://calendly.com/codeflash-saurabh/codeflash-setup"
target="_blank"
className="underline"
>
book a call
</a>{" "}
with the founder.
</p>
</>
) : (
<>
{" "}
<p className="leading-7 mt-6">
These API keys are used to authenticate your requests to Codeflash&apos;s AI services.
</p>
<ApiKeyTable apiKeys={apiKeys} vscodeKeyName={VS_CODE_KEY_NAME} />{" "}
</>
)}
<CreateApiKeyDialog />
</div>
</ApiKeysClient>
<CreateApiKeyDialog />
</div>
)
}

View file

@ -45,139 +45,132 @@ export async function getAllOptimizationEvents({
}
if (filter) {
// Merge filter into where, excluding review_quality since it's in a different table
Object.keys(filter).forEach(key => {
// COMMENTED OUT: Repository and quality filtering
// if (key === "repository_id") {
// where[key] = filter[key]
// } else if (key !== "review_quality") {
// where[key] = filter[key]
// }
// Only apply non-repository and non-quality filters
if (key !== "repository_id" && key !== "review_quality") {
if (key === "repository_id") {
where.AND = where.AND || []
where.AND.push({ [key]: filter[key] })
} else if (key !== "review_quality") {
where[key] = filter[key]
}
})
}
// COMMENTED OUT: Check if we need to join with optimization_features for quality sorting/filtering
// const needsOptimizationFeaturesJoin =
// (sort && Object.keys(sort).some(k => k.toLowerCase() === "review_quality")) ||
// (filter && Object.keys(filter).some(k => k.toLowerCase() === "review_quality"))
// Always use standard Prisma query (no quality joins)
const needsOptimizationFeaturesJoin = false
const needsOptimizationFeaturesJoin =
(sort && Object.keys(sort).some(k => k.toLowerCase() === "review_quality")) ||
(filter && Object.keys(filter).some(k => k.toLowerCase() === "review_quality"))
if (needsOptimizationFeaturesJoin) {
// ENTIRE BLOCK COMMENTED OUT - This was for quality filtering/sorting
// Build the WHERE clause for raw SQL
// const whereConditions = []
// const params: any[] = []
// let paramIndex = 1
// // Add is_staging condition
// whereConditions.push(`oe.is_staging = true`)
// // Add search conditions
// if (search) {
// whereConditions.push(
// `(oe.function_name ILIKE $${paramIndex} OR oe.file_path ILIKE $${paramIndex})`,
// )
// params.push(`%${search}%`)
// paramIndex += 1
// }
// // Add filter conditions
// if (filter) {
// if (filter.status) {
// whereConditions.push(`oe.status = $${paramIndex}`)
// params.push(filter.status)
// paramIndex += 1
// }
// if (filter.event_type) {
// whereConditions.push(`oe.event_type = $${paramIndex}`)
// params.push(filter.event_type)
// paramIndex += 1
// }
// if (filter.review_quality) {
// whereConditions.push(`of.review_quality = $${paramIndex}`)
// params.push(filter.review_quality)
// paramIndex += 1
// }
// if (filter.repository_id !== undefined) {
// if (filter.repository_id === null) {
// whereConditions.push(`oe.repository_id IS NULL`)
// } else if (filter.repository_id.not !== undefined && filter.repository_id.not === null) {
// whereConditions.push(`oe.repository_id IS NOT NULL`)
// }
// }
// }
// const whereClause = whereConditions.join(" AND ")
// // Build ORDER BY clause - handle all sort fields
// const orderByClauses: string[] = []
// if (sort && Object.keys(sort).length > 0) {
// Object.entries(sort).forEach(([key, direction]) => {
// const dir = direction.toUpperCase()
// if (key.toLowerCase() === "review_quality") {
// // Special handling for review_quality (case-insensitive)
// orderByClauses.push(`
// CASE
// WHEN LOWER(of.review_quality) = 'high' THEN 3
// WHEN LOWER(of.review_quality) = 'medium' THEN 2
// WHEN LOWER(of.review_quality) = 'low' THEN 1
// ELSE 0
// END ${dir}
// `)
// } else {
// // Regular field sorting
// orderByClauses.push(`oe.${key} ${dir}`)
// }
// })
// }
// // Add default sort by created_at only if sort is falsy
// if (!sort) {
// orderByClauses.push("oe.created_at DESC")
// }
// const orderByClause = orderByClauses.join(", ")
// // Get events with custom ordering - join with optimization_features table
// const events = await prisma.$queryRawUnsafe<any[]>(
// `
// SELECT
// oe.*,
// of.review_quality,
// of.review_explanation
// FROM optimization_events oe
// LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id
// WHERE ${whereClause}
// ORDER BY ${orderByClause}
// LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
// `,
// ...params,
// pageSize,
// (page - 1) * pageSize,
// )
// // Get total count
// const countResult = await prisma.$queryRawUnsafe<[{ count: bigint }]>(
// `
// SELECT COUNT(*) as count
// FROM optimization_events oe
// LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id
// WHERE ${whereClause}
// `,
// ...params,
// )
// const totalCount = Number(countResult[0].count)
// // Fetch repository data for the events
// const eventsWithRepo = await Promise.all(
// events.map(async event => {
// if (event.repository_id) {
// const repository = await prisma.repositories.findUnique({
// where: { id: event.repository_id },
// })
// return { ...event, repository }
// }
// return { ...event, repository: null }
// }),
// )
// return { events: eventsWithRepo, totalCount }
const whereConditions = []
const params: any[] = []
let paramIndex = 1
whereConditions.push(`oe.is_staging = true`)
if ("orgId" in payload) {
whereConditions.push(`oe.repository_id IN (${repoIds.map(id => `'${id}'`).join(",")})`)
} else {
whereConditions.push(
`(
oe.repository_id IN (${repoIds.map(id => `'${id}'`).join(",")})
OR oe.user_id = '${payload.userId}'
OR oe.current_username = '${payload.username}'
)`,
)
}
// Add search conditions
if (search) {
whereConditions.push(
`(oe.function_name ILIKE $${paramIndex} OR oe.file_path ILIKE $${paramIndex})`,
)
params.push(`%${search}%`)
paramIndex += 1
}
// Add filter conditions
if (filter) {
if (filter.status) {
whereConditions.push(`oe.status = $${paramIndex}`)
params.push(filter.status)
paramIndex += 1
}
if (filter.event_type) {
whereConditions.push(`oe.event_type = $${paramIndex}`)
params.push(filter.event_type)
paramIndex += 1
}
if (filter.review_quality) {
whereConditions.push(`of.review_quality = $${paramIndex}`)
params.push(filter.review_quality)
paramIndex += 1
}
if (filter.repository_id !== undefined) {
if (filter.repository_id === null) {
whereConditions.push(`oe.repository_id IS NULL`)
} else if (filter.repository_id.not !== undefined && filter.repository_id.not === null) {
whereConditions.push(`oe.repository_id IS NOT NULL`)
}
}
}
const whereClause = whereConditions.join(" AND ")
const orderByClauses: string[] = []
if (sort && Object.keys(sort).length > 0) {
Object.entries(sort).forEach(([key, direction]) => {
const dir = direction.toUpperCase()
if (key.toLowerCase() === "review_quality") {
orderByClauses.push(`
CASE
WHEN LOWER(of.review_quality) = 'high' THEN 3
WHEN LOWER(of.review_quality) = 'medium' THEN 2
WHEN LOWER(of.review_quality) = 'low' THEN 1
ELSE 0
END ${dir}
`)
} else {
orderByClauses.push(`oe.${key} ${dir}`)
}
})
}
if (!sort) {
orderByClauses.push("oe.created_at DESC")
}
const orderByClause = orderByClauses.join(", ")
const events = await prisma.$queryRawUnsafe<any[]>(
`
SELECT
oe.*,
of.review_quality,
of.review_explanation
FROM optimization_events oe
LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id
WHERE ${whereClause}
ORDER BY ${orderByClause}
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
`,
...params,
pageSize,
(page - 1) * pageSize,
)
// Get total count
const countResult = await prisma.$queryRawUnsafe<[{ count: bigint }]>(
`
SELECT COUNT(*) as count
FROM optimization_events oe
LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id
WHERE ${whereClause}
`,
...params,
)
const totalCount = Number(countResult[0].count)
// Fetch repository data for the events
const eventsWithRepo = await Promise.all(
events.map(async event => {
if (event.repository_id) {
const repository = await prisma.repositories.findUnique({
where: { id: event.repository_id },
})
return { ...event, repository }
}
return { ...event, repository: null }
}),
)
return { events: eventsWithRepo, totalCount }
} else {
// Standard Prisma query with native orderBy
const orderBy = sort || { created_at: "desc" }

View file

@ -1,5 +1,5 @@
"use client"
import { useState, useEffect, useCallback } from "react"
import { useState, useEffect, useCallback, useRef } from "react"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import {
@ -30,13 +30,12 @@ import {
ArrowDown,
} from "lucide-react"
import { formatDistanceToNow } from "date-fns"
import { useRouter, useSearchParams } from "next/navigation"
import { useRouter } from "next/navigation"
import { getUserId, getUserIdAndUsername } from "@/app/utils/auth"
import { Button } from "@/components/ui/button"
import { getAllOptimizationEvents } from "./action"
import Image from "next/image"
import { useViewMode } from "@/app/app/ViewModeContext"
// COMMENTED OUT: Quality badge import
import { ReviewQualityBadge } from "@/components/ui/quality_badge"
// Type definitions
@ -70,6 +69,66 @@ interface OptimizationEvent {
review_quality: string
}
interface FilterState {
search: string
hasRepo: string
status: string
eventType: string
reviewQuality: string
sortBy: string
page: number
}
function TableSkeleton() {
return (
<>
{Array.from({ length: 5 }).map((_, index) => (
<TableRow key={index}>
<TableCell>
<div className="flex items-start gap-3">
<div className="h-4 w-4 bg-muted animate-pulse rounded mt-1 flex-shrink-0" />
<div className="flex-1 min-w-0 space-y-2">
<div className="h-4 bg-muted animate-pulse rounded w-3/4" />
<div className="h-3 bg-muted animate-pulse rounded w-1/2" />
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-3">
<div className="h-8 w-8 bg-muted animate-pulse rounded-full flex-shrink-0" />
<div className="h-4 bg-muted animate-pulse rounded w-32" />
</div>
</TableCell>
<TableCell className="text-center">
<div className="h-6 bg-muted animate-pulse rounded-full w-20 mx-auto" />
</TableCell>
<TableCell className="text-center">
<div className="h-6 bg-muted animate-pulse rounded-full w-24 mx-auto" />
</TableCell>
<TableCell className="text-center">
<div className="h-6 bg-muted animate-pulse rounded-full w-16 mx-auto" />
</TableCell>
<TableCell className="text-center">
<div className="h-6 bg-muted animate-pulse rounded-full w-24 mx-auto" />
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-2">
<div className="h-6 bg-muted animate-pulse rounded-full w-12" />
<div className="h-6 bg-muted animate-pulse rounded-full w-12" />
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<div className="h-3 w-3 bg-muted animate-pulse rounded flex-shrink-0" />
<div className="h-4 bg-muted animate-pulse rounded w-24" />
</div>
</TableCell>
</TableRow>
))}
</>
)
}
// Helper function to calculate diff stats
function calculateDiffStats(
diffContents: Record<string, { oldContent: string; newContent: string }>,
@ -126,22 +185,6 @@ function calculateDiffStats(
}
}
// Helper function to calculate coverage percentage
function getCoverageInfo(diffContents: Record<string, { oldContent: string; newContent: string }>) {
// This is a simplified coverage calculation
// In practice, you'd want to integrate with actual coverage data
const { totalAdditions, totalDeletions } = calculateDiffStats(diffContents)
const totalLines = Object.values(diffContents).reduce((acc, { newContent }) => {
return acc + newContent.split("\n").filter(line => line.trim() !== "").length
}, 0)
// Simple heuristic: assume coverage decreases with more changes
const changeRatio = (totalAdditions + totalDeletions) / Math.max(totalLines, 1)
const coverage = Math.max(0.5, 1 - changeRatio * 0.3) // Keep between 50-100%
return Math.round(coverage * 100)
}
// Client component for handling row clicks
function ClickableTableRow({
event,
@ -152,13 +195,15 @@ function ClickableTableRow({
children: React.ReactNode
onRowClick: (eventId: string) => void
}) {
const handleRowClick = (e: React.MouseEvent) => {
// Don't navigate if clicking on external link
if ((e.target as HTMLElement).closest('a[href^="http"]')) {
return
}
onRowClick(event.id)
}
const handleRowClick = useCallback(
(e: React.MouseEvent) => {
if ((e.target as HTMLElement).closest('a[href^="http"]')) {
return
}
onRowClick(event.trace_id)
},
[event.trace_id, onRowClick],
)
return (
<TableRow
@ -175,37 +220,32 @@ export default function StagingPage() {
const [userId, setUserId] = useState<string | null>(null)
const [isLoadingUser, setIsLoadingUser] = useState(true)
const router = useRouter()
const searchParams = useSearchParams()
const [events, setEvents] = useState<OptimizationEvent[]>([])
const [totalCount, setTotalCount] = useState(0)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
// Local page state instead of URL-based
const [page, setPage] = useState(1)
const search = searchParams.get("search") || ""
// COMMENTED OUT: Repository filter from URL
// const hasRepo = searchParams.get("hasRepo") || "all"
const status = searchParams.get("status") || "all"
const eventType = searchParams.get("eventType") || "all"
// COMMENTED OUT: Quality filter from URL
// const reviewQuality = searchParams.get("reviewQuality") || "all"
const sortBy = searchParams.get("sortBy") || "created_at_desc"
const pageSize = 10
// Local state for filter inputs
const [searchInput, setSearchInput] = useState(search)
// COMMENTED OUT: Repository filter state
// const [hasRepoFilter, setHasRepoFilter] = useState(hasRepo)
const [statusFilter, setStatusFilter] = useState(status)
const [eventTypeFilter, setEventTypeFilter] = useState(eventType)
// COMMENTED OUT: Quality filter state
// const [reviewQualityFilter, setReviewQualityFilter] = useState(reviewQuality)
const [sortByFilter, setSortByFilter] = useState(sortBy)
const { currentOrg } = useViewMode()
// Load user ID on mount
// Combined filter state
const [filters, setFilters] = useState<FilterState>({
search: "",
hasRepo: "all",
status: "all",
eventType: "all",
reviewQuality: "all",
sortBy: "created_at_desc",
page: 1,
})
const pageSize = 10
// Refs to track if initial load is done
const isInitialMount = useRef(true)
const debounceTimer = useRef<NodeJS.Timeout>()
// Load user ID on mount - only once
useEffect(() => {
const loadUserId = async () => {
try {
@ -221,6 +261,7 @@ export default function StagingPage() {
loadUserId()
}, [])
// Memoized load events function
const loadEvents = useCallback(async () => {
if (!userId) return
@ -230,28 +271,26 @@ export default function StagingPage() {
try {
const filter: Record<string, any> = {}
// COMMENTED OUT: Repository filter logic
// if (hasRepo === "yes") {
// filter.repository_id = { not: null }
// } else if (hasRepo === "no") {
// filter.repository_id = null
// }
if (status !== "all") {
filter.status = status
if (filters.hasRepo === "yes") {
filter.repository_id = { not: null }
} else if (filters.hasRepo === "no") {
filter.repository_id = null
}
if (eventType !== "all") {
filter.event_type = eventType
if (filters.status !== "all") {
filter.status = filters.status
}
// COMMENTED OUT: Quality filter logic
// if (reviewQuality !== "all") {
// filter.review_quality = reviewQuality
// }
if (filters.eventType !== "all") {
filter.event_type = filters.eventType
}
if (filters.reviewQuality !== "all") {
filter.review_quality = filters.reviewQuality
}
// Parse sort parameter
const [sortField, sortDirection] = sortBy.split("_").reduce(
const [sortField, sortDirection] = filters.sortBy.split("_").reduce(
(acc, part, index, arr) => {
if (index === arr.length - 1 && (part === "asc" || part === "desc")) {
return [acc[0], part]
@ -264,15 +303,16 @@ export default function StagingPage() {
const sort: Record<string, "asc" | "desc"> = {
[sortField]: sortDirection as "asc" | "desc",
}
const userSession = await getUserIdAndUsername()
const data = await getAllOptimizationEvents({
payload: currentOrg
? { orgId: currentOrg.id }
: { userId: userSession.userId, username: userSession.username },
search,
search: filters.search,
filter,
sort,
page,
page: filters.page,
pageSize,
})
@ -295,105 +335,116 @@ export default function StagingPage() {
} finally {
setIsLoading(false)
}
}, [
userId,
search,
// COMMENTED OUT: hasRepo dependency
// hasRepo,
status,
eventType,
// COMMENTED OUT: reviewQuality dependency
// reviewQuality,
sortBy,
page,
pageSize,
currentOrg,
])
}, [userId, filters, currentOrg, pageSize])
// Load events when filters change - with debounce for search
useEffect(() => {
if (!isLoadingUser && userId) {
loadEvents()
// Skip initial mount
if (isInitialMount.current) {
isInitialMount.current = false
loadEvents()
return
}
// Clear existing timer
if (debounceTimer.current) {
clearTimeout(debounceTimer.current)
}
// Debounce only for search changes
const hasSearchChanged = filters.search !== ""
if (hasSearchChanged) {
debounceTimer.current = setTimeout(() => {
loadEvents()
}, 300)
} else {
loadEvents()
}
}
}, [userId, isLoadingUser, loadEvents])
const handleRowClick = (traceId: string) => {
router.push(`/review-optimizations/${traceId}`)
}
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current)
}
}
}, [userId, isLoadingUser, filters, loadEvents])
// Update URL when filters change
useEffect(() => {
const timer = setTimeout(() => {
const params = new URLSearchParams()
if (searchInput) params.set("search", searchInput)
// COMMENTED OUT: Repository filter param
// if (hasRepoFilter !== "all") params.set("hasRepo", hasRepoFilter)
if (statusFilter !== "all") params.set("status", statusFilter)
if (eventTypeFilter !== "all") params.set("eventType", eventTypeFilter)
// COMMENTED OUT: Quality filter param
// if (reviewQualityFilter !== "all") params.set("reviewQuality", reviewQualityFilter)
if (sortByFilter !== "created_at_desc") params.set("sortBy", sortByFilter)
const handleRowClick = useCallback(
(traceId: string) => {
router.push(`/review-optimizations/${traceId}`)
},
[router],
)
// Reset to page 1 when filters change
setPage(1)
router.push(`?${params.toString()}`)
}, 300) // 300ms debounce
// Update filter functions
const updateFilter = useCallback((key: keyof FilterState, value: string | number) => {
setFilters(prev => ({
...prev,
[key]: value,
// Reset page when filters change (except page itself)
...(key !== "page" && { page: 1 }),
}))
}, [])
return () => clearTimeout(timer)
}, [
searchInput,
// COMMENTED OUT: hasRepoFilter dependency
// hasRepoFilter,
statusFilter,
eventTypeFilter,
// COMMENTED OUT: reviewQualityFilter dependency
// reviewQualityFilter,
sortByFilter,
router,
])
const clearFilters = () => {
setSearchInput("")
// COMMENTED OUT: Reset repository filter
// setHasRepoFilter("all")
setStatusFilter("all")
setEventTypeFilter("all")
// COMMENTED OUT: Reset quality filter
// setReviewQualityFilter("all")
setSortByFilter("created_at_desc")
setPage(1)
router.push("/review-optimizations")
}
const clearFilters = useCallback(() => {
setFilters({
search: "",
hasRepo: "all",
status: "all",
eventType: "all",
reviewQuality: "all",
sortBy: "created_at_desc",
page: 1,
})
}, [])
const hasActiveFilters =
searchInput ||
// COMMENTED OUT: hasRepoFilter check
// hasRepoFilter !== "all" ||
statusFilter !== "all" ||
eventTypeFilter !== "all" ||
// COMMENTED OUT: reviewQualityFilter check
// reviewQualityFilter !== "all" ||
sortByFilter !== "created_at_desc"
filters.search ||
filters.hasRepo !== "all" ||
filters.status !== "all" ||
filters.eventType !== "all" ||
filters.reviewQuality !== "all" ||
filters.sortBy !== "created_at_desc"
const totalPages = Math.ceil(totalCount / pageSize)
const handlePageChange = (newPage: number) => {
if (newPage >= 1 && newPage <= totalPages) {
setPage(newPage)
}
}
const handlePageChange = useCallback(
(newPage: number) => {
if (newPage >= 1 && newPage <= totalPages) {
updateFilter("page", newPage)
}
},
[totalPages, updateFilter],
)
const getSortIcon = (field: string) => {
if (sortByFilter.startsWith(field)) {
return sortByFilter.endsWith("_asc") ? (
<ArrowUp className="h-4 w-4" />
) : (
<ArrowDown className="h-4 w-4" />
)
}
return <ArrowUpDown className="h-4 w-4 opacity-50" />
}
const getSortIcon = useCallback(
(field: string) => {
if (filters.sortBy.startsWith(field)) {
return filters.sortBy.endsWith("_asc") ? (
<ArrowUp className="h-4 w-4" />
) : (
<ArrowDown className="h-4 w-4" />
)
}
return <ArrowUpDown className="h-4 w-4 opacity-50" />
},
[filters.sortBy],
)
const getSpeedupBadge = (speedup?: number, speedupPct?: number) => {
const toggleSort = useCallback(
(field: string) => {
const newSort = filters.sortBy.startsWith(field)
? filters.sortBy === `${field}_desc`
? `${field}_asc`
: `${field}_desc`
: `${field}_desc`
updateFilter("sortBy", newSort)
},
[filters.sortBy, updateFilter],
)
const getSpeedupBadge = useCallback((speedup?: number, speedupPct?: number) => {
if (typeof speedup !== "number" || typeof speedupPct !== "number") return null
const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max)
@ -423,9 +474,9 @@ export default function StagingPage() {
{speedup.toFixed(2)}x ({speedupPct.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")}%)
</Badge>
)
}
}, [])
const getStatusBadge = (status?: string) => {
const getStatusBadge = useCallback((status?: string) => {
if (!status) return null
const variants: Record<string, { className: string; label: string }> = {
@ -451,9 +502,9 @@ export default function StagingPage() {
{variant.label}
</Badge>
)
}
}, [])
const getEventTypeBadge = (eventType?: string) => {
const getEventTypeBadge = useCallback((eventType?: string) => {
if (!eventType) return null
const variants: Record<string, { className: string; label: string }> = {
@ -489,7 +540,7 @@ export default function StagingPage() {
{variant.label}
</Badge>
)
}
}, [])
if (isLoadingUser) {
return (
@ -523,8 +574,8 @@ export default function StagingPage() {
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder="Search by function name or file path..."
value={searchInput}
onChange={e => setSearchInput(e.target.value)}
value={filters.search}
onChange={e => updateFilter("search", e.target.value)}
className="pl-10 w-full"
/>
</div>
@ -536,19 +587,18 @@ export default function StagingPage() {
</div>
{/* Filter Controls */}
{/* <Select value={hasRepoFilter} onValueChange={setHasRepoFilter}>
<Select value={filters.hasRepo} onValueChange={value => updateFilter("hasRepo", value)}>
<SelectTrigger className="w-[140px] sm:w-[180px]">
<SelectValue placeholder="Repository" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Repositories</SelectItem>
<SelectItem value="yes">Has Repository</SelectItem>
<SelectItem value="no">No Repository</SelectItem>
<SelectItem value="yes">With Repository</SelectItem>
<SelectItem value="no">Without Repository</SelectItem>
</SelectContent>
</Select> */}
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<Select value={filters.status} onValueChange={value => updateFilter("status", value)}>
<SelectTrigger className="w-[120px] sm:w-[150px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
@ -559,7 +609,10 @@ export default function StagingPage() {
</SelectContent>
</Select>
<Select value={eventTypeFilter} onValueChange={setEventTypeFilter}>
<Select
value={filters.eventType}
onValueChange={value => updateFilter("eventType", value)}
>
<SelectTrigger className="w-[120px] sm:w-[150px]">
<SelectValue placeholder="Event Type" />
</SelectTrigger>
@ -572,20 +625,22 @@ export default function StagingPage() {
</SelectContent>
</Select>
{/* COMMENTED OUT: Quality Filter */}
{/* <Select value={reviewQualityFilter} onValueChange={setReviewQualityFilter}>
<Select
value={filters.reviewQuality}
onValueChange={value => updateFilter("reviewQuality", value)}
>
<SelectTrigger className="w-[120px] sm:w-[150px]">
<SelectValue placeholder="Quality" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All qualities</SelectItem>
<SelectItem value="all">All Quality</SelectItem>
<SelectItem value="high">High</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="low">Low</SelectItem>
</SelectContent>
</Select> */}
</Select>
<Select value={sortByFilter} onValueChange={setSortByFilter}>
<Select value={filters.sortBy} onValueChange={value => updateFilter("sortBy", value)}>
<SelectTrigger className="w-[140px] sm:w-[200px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
@ -594,9 +649,8 @@ export default function StagingPage() {
<SelectItem value="created_at_asc">Oldest</SelectItem>
<SelectItem value="speedup_x_desc">Speedup (Highest)</SelectItem>
<SelectItem value="speedup_x_asc">Speedup (Lowest)</SelectItem>
{/* COMMENTED OUT: Quality sort options */}
{/* <SelectItem value="review_quality_desc">Quality (High to Low)</SelectItem>
<SelectItem value="review_quality_asc">Quality (Low to High)</SelectItem> */}
<SelectItem value="review_quality_desc">Quality (High to Low)</SelectItem>
<SelectItem value="review_quality_asc">Quality (Low to High)</SelectItem>
</SelectContent>
</Select>
@ -638,33 +692,18 @@ export default function StagingPage() {
<TableHead className="w-[18%]">REPOSITORY</TableHead>
<TableHead className="text-center">REVIEW</TableHead>
<TableHead className="text-center">STATUS</TableHead>
{/* COMMENTED OUT: Quality column header with sorting */}
<TableHead
className="text-center cursor-pointer hover:bg-muted/50"
// onClick={() => {
// const newSort = sortByFilter.startsWith("review_quality")
// ? sortByFilter === "review_quality_desc"
// ? "review_quality_asc"
// : "review_quality_desc"
// : "review_quality_desc"
// setSortByFilter(newSort)
// }}
onClick={() => toggleSort("review_quality")}
>
<div className="flex items-center justify-center gap-1">
<span>QUALITY</span>
{/* {getSortIcon("review_quality")} */}
{getSortIcon("review_quality")}
</div>
</TableHead>
<TableHead
className="text-center cursor-pointer hover:bg-muted/50"
onClick={() => {
const newSort = sortByFilter.startsWith("speedup_x")
? sortByFilter === "speedup_x_desc"
? "speedup_x_asc"
: "speedup_x_desc"
: "speedup_x_desc"
setSortByFilter(newSort)
}}
onClick={() => toggleSort("speedup_x")}
>
<div className="flex items-center justify-center gap-1">
<span>SPEEDUP</span>
@ -674,14 +713,7 @@ export default function StagingPage() {
<TableHead className="text-center">CHANGES</TableHead>
<TableHead
className="text-right cursor-pointer hover:bg-muted/50"
onClick={() => {
const newSort = sortByFilter.startsWith("created_at")
? sortByFilter === "created_at_desc"
? "created_at_asc"
: "created_at_desc"
: "created_at_desc"
setSortByFilter(newSort)
}}
onClick={() => toggleSort("created_at")}
>
<div className="flex items-center justify-end gap-1">
<span>CREATED AT</span>
@ -691,7 +723,9 @@ export default function StagingPage() {
</TableRow>
</TableHeader>
<TableBody>
{events.length === 0 ? (
{isLoading ? (
<TableSkeleton />
) : events.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center py-12">
<div className="text-muted-foreground">
@ -708,7 +742,7 @@ export default function StagingPage() {
totalAdditions: 0,
totalDeletions: 0,
}
let coverage: number | null = null
if (
event.metadata &&
typeof event.metadata === "object" &&
@ -717,7 +751,6 @@ export default function StagingPage() {
event.metadata.diffContents !== null
) {
const diffContentsRaw = event.metadata.diffContents
// Check that every value is an object with oldContent and newContent as strings
if (diffContentsRaw && typeof diffContentsRaw === "object") {
let valid = true
for (const value of Object.values(diffContentsRaw as object)) {
@ -737,19 +770,12 @@ export default function StagingPage() {
{ oldContent: string; newContent: string }
>
diffStats = calculateDiffStats(diffContents)
coverage = getCoverageInfo(diffContents)
}
}
}
// fallback for coverage if not available
if (coverage === null) coverage = 100
return (
<ClickableTableRow
key={event.id}
event={event}
onRowClick={() => handleRowClick(event.trace_id)}
>
<ClickableTableRow key={event.id} event={event} onRowClick={handleRowClick}>
<TableCell className="w-auto min-w-0">
<div className="flex items-start gap-3">
<FileCode2 className="h-4 w-4 text-muted-foreground mt-1 flex-shrink-0" />
@ -767,7 +793,6 @@ export default function StagingPage() {
<div className="flex items-center gap-3">
{event.repository ? (
<>
{/* Repository image */}
<div className="relative h-8 w-8 flex-shrink-0">
{event.repository.full_name && (
<Image
@ -777,13 +802,10 @@ export default function StagingPage() {
className="rounded-full object-cover"
onError={e => {
e.currentTarget.style.display = "none"
e.currentTarget.nextElementSibling?.classList.remove("hidden")
}}
/>
)}
</div>
{/* Repository name */}
<div className="flex-1 min-w-0 overflow-hidden">
<div className="flex items-center gap-1">
<span className="text-sm font-medium truncate">
@ -794,7 +816,6 @@ export default function StagingPage() {
</>
) : (
<>
{/* Placeholder for untracked optimization */}
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
<Zap className="h-4 w-4 text-muted-foreground" />
</div>
@ -856,18 +877,19 @@ export default function StagingPage() {
</Table>
</div>
{totalPages > 1 && (
{/* Pagination */}
{!isLoading && totalPages > 1 && (
<div className="flex items-center justify-between mt-6">
<p className="text-sm text-muted-foreground">
Showing {(page - 1) * pageSize + 1} to {Math.min(page * pageSize, totalCount)} of{" "}
{totalCount} events
Showing {(filters.page - 1) * pageSize + 1} to{" "}
{Math.min(filters.page * pageSize, totalCount)} of {totalCount} events
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={page === 1}
onClick={() => handlePageChange(page - 1)}
disabled={filters.page === 1}
onClick={() => handlePageChange(filters.page - 1)}
>
<ChevronLeft className="h-4 w-4" />
Previous
@ -879,18 +901,18 @@ export default function StagingPage() {
if (totalPages <= 5) {
pageNum = i + 1
} else if (page <= 3) {
} else if (filters.page <= 3) {
pageNum = i + 1
} else if (page >= totalPages - 2) {
} else if (filters.page >= totalPages - 2) {
pageNum = totalPages - 4 + i
} else {
pageNum = page - 2 + i
pageNum = filters.page - 2 + i
}
return (
<Button
key={i}
variant={page === pageNum ? "default" : "outline"}
variant={filters.page === pageNum ? "default" : "outline"}
size="sm"
className="w-8 h-8 p-0"
onClick={() => handlePageChange(pageNum)}
@ -899,8 +921,8 @@ export default function StagingPage() {
</Button>
)
})}
{totalPages > 5 && page < totalPages - 2 && <span className="px-2">...</span>}
{totalPages > 5 && page < totalPages - 2 && (
{totalPages > 5 && filters.page < totalPages - 2 && <span className="px-2">...</span>}
{totalPages > 5 && filters.page < totalPages - 2 && (
<Button
variant="outline"
size="sm"
@ -915,8 +937,8 @@ export default function StagingPage() {
<Button
variant="outline"
size="sm"
disabled={page === totalPages}
onClick={() => handlePageChange(page + 1)}
disabled={filters.page === totalPages}
onClick={() => handlePageChange(filters.page + 1)}
>
Next
<ChevronRight className="h-4 w-4" />